diff --git a/packages/state-transition/src/stateView/interface.ts b/packages/state-transition/src/stateView/interface.ts index 5dead7e6dffa..e1905d807770 100644 --- a/packages/state-transition/src/stateView/interface.ts +++ b/packages/state-transition/src/stateView/interface.ts @@ -278,6 +278,35 @@ export type IBeaconStateViewLatestFork = Omit< latestExecutionPayloadHeader: ExecutionPayloadHeader; payloadBlockNumber: number; }; + +/** + * Contract a BeaconStateView backing implementation must satisfy. + * + * Differs from `IBeaconStateViewLatestFork` in two ways: + * - `executionPayloadAvailability` is a raw `{uint8Array, bitLen}` POJO — a + * native (`.node`) binding cannot construct a `BitArray` across FFI. + * `NativeBeaconStateView` lifts it back to `BitArray` for beacon-node. + * - Methods that produce another view (`stateTransition`, `processSlots`, + * `loadOtherState`, `withParentPayloadApplied`) return `IBeaconStateViewNative` + * so callers can re-wrap without an `as unknown` cast. Param lists are reused + * via `Parameters<...>` to avoid duplicating signatures. + * + * The TS-side `BeaconStateView` also structurally satisfies this contract since + * `BitArray` exposes `uint8Array` and `bitLen`. + */ +export type IBeaconStateViewNative = Omit< + IBeaconStateViewLatestFork, + "executionPayloadAvailability" | "loadOtherState" | "stateTransition" | "processSlots" | "withParentPayloadApplied" +> & { + executionPayloadAvailability: {uint8Array: Uint8Array; bitLen: number}; + loadOtherState(...args: Parameters): IBeaconStateViewNative; + stateTransition(...args: Parameters): IBeaconStateViewNative; + processSlots(...args: Parameters): IBeaconStateViewNative; + withParentPayloadApplied( + ...args: Parameters + ): IBeaconStateViewNative; +}; + export function isStatePostAltair(state: IBeaconStateView): state is IBeaconStateViewAltair { return isForkPostAltair(state.forkName); } diff --git a/packages/state-transition/src/stateView/nativeBeaconStateView.ts b/packages/state-transition/src/stateView/nativeBeaconStateView.ts new file mode 100644 index 000000000000..c0f3ad4a967a --- /dev/null +++ b/packages/state-transition/src/stateView/nativeBeaconStateView.ts @@ -0,0 +1,893 @@ +import {CompactMultiProof} from "@chainsafe/persistent-merkle-tree"; +import {BitArray, ByteViews} from "@chainsafe/ssz"; +import {ForkName} from "@lodestar/params"; +import { + BeaconBlock, + BeaconState, + BlindedBeaconBlock, + BuilderIndex, + Bytes32, + Epoch, + ExecutionPayloadBid, + ExecutionPayloadHeader, + Root, + RootHex, + SignedBeaconBlock, + SignedBlindedBeaconBlock, + Slot, + SyncCommittee, + 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"; +import { + IBeaconStateView, + IBeaconStateViewGloas, + IBeaconStateViewLatestFork, + IBeaconStateViewNative, + isStatePostGloas, +} from "./interface.js"; + +/** + * Wraps a native binding (the auto-generated JS interface produced by a `.node` + * file) and exposes it as a fully-conformant `IBeaconStateViewLatestFork`. + * + * The binding is typed `IBeaconStateViewNative` — identical to + * `IBeaconStateViewLatestFork` except `executionPayloadAvailability` is a raw + * `{uint8Array, bitLen}` POJO. The `executionPayloadAvailability` getter lifts + * that POJO back to a `BitArray` so beacon-node consumers see no difference from + * the TS-side `BeaconStateView`. + * + * Every getter that returns a value stable for the view's lifetime is cached so + * the binding is hit at most once per field per view. Only mutable counters + * (`proposerRewards`, `clonedCount`, `clonedCountWithTransferCache`) stay + * pass-through. Methods with arguments are pass-through too — caching them + * would need a per-arg map and isn't worth it without a hot-path signal. + */ +export class NativeBeaconStateView implements IBeaconStateViewLatestFork { + // phase0 + private _forkName: ForkName | null = null; + private _slot: Slot | null = null; + private _fork: Fork | null = null; + private _epoch: Epoch | null = null; + private _genesisTime: number | null = null; + private _genesisValidatorsRoot: Root | null = null; + private _eth1Data: phase0.Eth1Data | null = null; + private _latestBlockHeader: phase0.BeaconBlockHeader | null = null; + private _previousJustifiedCheckpoint: Checkpoint | null = null; + private _currentJustifiedCheckpoint: Checkpoint | null = null; + private _finalizedCheckpoint: Checkpoint | null = null; + // shuffling / decision roots / proposers + private _previousDecisionRoot: RootHex | null = null; + private _currentDecisionRoot: RootHex | null = null; + private _nextDecisionRoot: RootHex | null = null; + // previousProposers can be null, so use undefined as the "not loaded" sentinel + private _previousProposers: ValidatorIndex[] | null | undefined = undefined; + private _currentProposers: ValidatorIndex[] | null = null; + private _nextProposers: ValidatorIndex[] | null = null; + // validators / balances + private _effectiveBalanceIncrements: EffectiveBalanceIncrements | null = null; + private _validatorCount: number | null = null; + private _activeValidatorCount: number | null = null; + // backward compat + private _createdWithTransferCache: boolean | null = null; + // altair + private _currentSyncCommittee: SyncCommittee | null = null; + private _nextSyncCommittee: SyncCommittee | null = null; + private _previousEpochParticipation: Uint8Array | null = null; + private _currentEpochParticipation: Uint8Array | null = null; + private _currentSyncCommitteeIndexed: SyncCommitteeCache | null = null; + private _syncProposerReward: number | null = null; + // bellatrix + private _latestExecutionPayloadHeader: ExecutionPayloadHeader | null = null; + private _payloadBlockNumber: number | null = null; + private _isExecutionStateType: boolean | null = null; + private _isMergeTransitionComplete: boolean | 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; + private _pendingDepositsCount: number | null = null; + private _pendingPartialWithdrawalsCount: number | null = null; + private _pendingConsolidationsCount: number | null = null; + // fulu + private _proposerLookahead: fulu.ProposerLookahead | null = null; + // gloas + private _executionPayloadAvailability: BitArray | null = null; + private _latestBlockHash: Bytes32 | null = null; + private _latestExecutionPayloadBid: ExecutionPayloadBid | null = null; + private _payloadExpectedWithdrawals: capella.Withdrawal[] | null = null; + + // Per-argument caches for argument-taking methods. The binding is treated as + // immutable for the view's lifetime, so a given argument always yields the + // same result. Maps grow only with touched arguments — typical call patterns + // (e.g. a handful of slots per attestation pool scan) keep them tiny. + private readonly _getBlockRootAtSlot = new Map(); + private readonly _getBlockRootAtEpoch = new Map(); + private readonly _getStateRootAtSlot = new Map(); + private readonly _getRandaoMix = new Map(); + private readonly _getShufflingAtEpoch = new Map(); + private readonly _getShufflingDecisionRoot = new Map(); + private readonly _getBeaconProposer = new Map(); + // getBeaconProposerOrNull can return null, so use .has() to distinguish "not cached" from "cached null" + private readonly _getBeaconProposerOrNull = new Map(); + private readonly _getValidator = new Map(); + private readonly _getBalance = new Map(); + private readonly _getIndexedSyncCommitteeAtEpoch = new Map(); + private readonly _getIndexedSyncCommittee = new Map(); + private readonly _getSingleProof = new Map(); + private readonly _getEpochPTCs = new Map(); + private readonly _getBuilder = new Map(); + + // No-arg method caches + private _getPreviousShuffling: EpochShuffling | null = null; + private _getCurrentShuffling: EpochShuffling | null = null; + private _getNextShuffling: EpochShuffling | null = null; + private _getEffectiveBalanceIncrementsZeroInactive: EffectiveBalanceIncrements | null = null; + private _getAllValidators: phase0.Validator[] | null = null; + private _getAllBalances: number[] | null = null; + private _getLatestWeakSubjectivityCheckpointEpoch: Epoch | null = null; + private _getFinalizedRootProof: Uint8Array[] | null = null; + private _computeUnrealizedCheckpoints: { + justifiedCheckpoint: phase0.Checkpoint; + finalizedCheckpoint: phase0.Checkpoint; + } | null = null; + private _computeAnchorCheckpoint: {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} | null = + null; + private _isStateValidatorsNodesPopulated: boolean | null = null; + private _toValue: BeaconState | null = null; + private _serialize: Uint8Array | null = null; + private _serializedSize: number | null = null; + private _serializeValidators: Uint8Array | null = null; + private _serializedValidatorsSize: number | null = null; + private _hashTreeRoot: Uint8Array | null = null; + private _getSyncCommitteesWitness: SyncCommitteeWitness | null = null; + private _getExpectedWithdrawals: { + expectedWithdrawals: capella.Withdrawal[]; + processedBuilderWithdrawalsCount: number; + processedPartialWithdrawalsCount: number; + processedBuildersSweepCount: number; + processedValidatorSweepCount: number; + } | null = null; + + constructor(readonly binding: IBeaconStateViewNative) {} + + // Binding returns pojo object {uint8Array: Uint8Array; bitLen: number} + // this class wrap it with BitArray to conform to the api + get executionPayloadAvailability(): BitArray { + if (this._executionPayloadAvailability === null) { + const pojo = this.binding.executionPayloadAvailability; + this._executionPayloadAvailability = new BitArray(pojo.uint8Array, pojo.bitLen); + } + + return this._executionPayloadAvailability; + } + + // ─── phase0 ────────────────────────────────────────────────────────────── + + get forkName(): ForkName { + if (this._forkName === null) { + this._forkName = this.binding.forkName; + } + return this._forkName; + } + + get slot(): Slot { + if (this._slot === null) { + this._slot = this.binding.slot; + } + return this._slot; + } + + get fork(): Fork { + if (this._fork === null) { + this._fork = this.binding.fork; + } + return this._fork; + } + + get epoch(): Epoch { + if (this._epoch === null) { + this._epoch = this.binding.epoch; + } + return this._epoch; + } + + get genesisTime(): number { + if (this._genesisTime === null) { + this._genesisTime = this.binding.genesisTime; + } + return this._genesisTime; + } + + get genesisValidatorsRoot(): Root { + if (this._genesisValidatorsRoot === null) { + this._genesisValidatorsRoot = this.binding.genesisValidatorsRoot; + } + return this._genesisValidatorsRoot; + } + + get eth1Data(): phase0.Eth1Data { + if (this._eth1Data === null) { + this._eth1Data = this.binding.eth1Data; + } + return this._eth1Data; + } + + get latestBlockHeader(): phase0.BeaconBlockHeader { + if (this._latestBlockHeader === null) { + this._latestBlockHeader = this.binding.latestBlockHeader; + } + return this._latestBlockHeader; + } + + get previousJustifiedCheckpoint(): Checkpoint { + if (this._previousJustifiedCheckpoint === null) { + this._previousJustifiedCheckpoint = this.binding.previousJustifiedCheckpoint; + } + return this._previousJustifiedCheckpoint; + } + + get currentJustifiedCheckpoint(): Checkpoint { + if (this._currentJustifiedCheckpoint === null) { + this._currentJustifiedCheckpoint = this.binding.currentJustifiedCheckpoint; + } + return this._currentJustifiedCheckpoint; + } + + get finalizedCheckpoint(): Checkpoint { + if (this._finalizedCheckpoint === null) { + this._finalizedCheckpoint = this.binding.finalizedCheckpoint; + } + return this._finalizedCheckpoint; + } + + getBlockRootAtSlot(slot: Slot): Root { + let cached = this._getBlockRootAtSlot.get(slot); + if (cached === undefined) { + cached = this.binding.getBlockRootAtSlot(slot); + this._getBlockRootAtSlot.set(slot, cached); + } + return cached; + } + + getBlockRootAtEpoch(epoch: Epoch): Root { + let cached = this._getBlockRootAtEpoch.get(epoch); + if (cached === undefined) { + cached = this.binding.getBlockRootAtEpoch(epoch); + this._getBlockRootAtEpoch.set(epoch, cached); + } + return cached; + } + + getStateRootAtSlot(slot: Slot): Root { + let cached = this._getStateRootAtSlot.get(slot); + if (cached === undefined) { + cached = this.binding.getStateRootAtSlot(slot); + this._getStateRootAtSlot.set(slot, cached); + } + return cached; + } + + getRandaoMix(epoch: Epoch): Bytes32 { + let cached = this._getRandaoMix.get(epoch); + if (cached === undefined) { + cached = this.binding.getRandaoMix(epoch); + this._getRandaoMix.set(epoch, cached); + } + return cached; + } + + // Shuffling and committees + + getShufflingAtEpoch(epoch: Epoch): EpochShuffling { + let cached = this._getShufflingAtEpoch.get(epoch); + if (cached === undefined) { + cached = this.binding.getShufflingAtEpoch(epoch); + this._getShufflingAtEpoch.set(epoch, cached); + } + return cached; + } + + get previousDecisionRoot(): RootHex { + if (this._previousDecisionRoot === null) { + this._previousDecisionRoot = this.binding.previousDecisionRoot; + } + return this._previousDecisionRoot; + } + + get currentDecisionRoot(): RootHex { + if (this._currentDecisionRoot === null) { + this._currentDecisionRoot = this.binding.currentDecisionRoot; + } + return this._currentDecisionRoot; + } + + get nextDecisionRoot(): RootHex { + if (this._nextDecisionRoot === null) { + this._nextDecisionRoot = this.binding.nextDecisionRoot; + } + return this._nextDecisionRoot; + } + + getShufflingDecisionRoot(epoch: Epoch): RootHex { + let cached = this._getShufflingDecisionRoot.get(epoch); + if (cached === undefined) { + cached = this.binding.getShufflingDecisionRoot(epoch); + this._getShufflingDecisionRoot.set(epoch, cached); + } + return cached; + } + + getPreviousShuffling(): EpochShuffling { + if (this._getPreviousShuffling === null) { + this._getPreviousShuffling = this.binding.getPreviousShuffling(); + } + return this._getPreviousShuffling; + } + + getCurrentShuffling(): EpochShuffling { + if (this._getCurrentShuffling === null) { + this._getCurrentShuffling = this.binding.getCurrentShuffling(); + } + return this._getCurrentShuffling; + } + + getNextShuffling(): EpochShuffling { + if (this._getNextShuffling === null) { + this._getNextShuffling = this.binding.getNextShuffling(); + } + return this._getNextShuffling; + } + + // Proposer shuffling + + get previousProposers(): ValidatorIndex[] | null { + if (this._previousProposers === undefined) { + this._previousProposers = this.binding.previousProposers; + } + return this._previousProposers; + } + + get currentProposers(): ValidatorIndex[] { + if (this._currentProposers === null) { + this._currentProposers = this.binding.currentProposers; + } + return this._currentProposers; + } + + get nextProposers(): ValidatorIndex[] { + if (this._nextProposers === null) { + this._nextProposers = this.binding.nextProposers; + } + return this._nextProposers; + } + + getBeaconProposer(slot: Slot): ValidatorIndex { + let cached = this._getBeaconProposer.get(slot); + if (cached === undefined) { + cached = this.binding.getBeaconProposer(slot); + this._getBeaconProposer.set(slot, cached); + } + return cached; + } + + getBeaconProposerOrNull(slot: Slot): ValidatorIndex | null { + if (!this._getBeaconProposerOrNull.has(slot)) { + this._getBeaconProposerOrNull.set(slot, this.binding.getBeaconProposerOrNull(slot)); + } + // biome-ignore lint/style/noNonNullAssertion: has() check guarantees a value + return this._getBeaconProposerOrNull.get(slot)!; + } + + // Validators and balances + + get effectiveBalanceIncrements(): EffectiveBalanceIncrements { + if (this._effectiveBalanceIncrements === null) { + this._effectiveBalanceIncrements = this.binding.effectiveBalanceIncrements; + } + return this._effectiveBalanceIncrements; + } + + getEffectiveBalanceIncrementsZeroInactive(): EffectiveBalanceIncrements { + if (this._getEffectiveBalanceIncrementsZeroInactive === null) { + this._getEffectiveBalanceIncrementsZeroInactive = this.binding.getEffectiveBalanceIncrementsZeroInactive(); + } + return this._getEffectiveBalanceIncrementsZeroInactive; + } + + getBalance(index: number): number { + let cached = this._getBalance.get(index); + if (cached === undefined) { + cached = this.binding.getBalance(index); + this._getBalance.set(index, cached); + } + return cached; + } + + getValidator(index: ValidatorIndex): phase0.Validator { + let cached = this._getValidator.get(index); + if (cached === undefined) { + cached = this.binding.getValidator(index); + this._getValidator.set(index, cached); + } + return cached; + } + + getValidatorsByStatus(statuses: Set, currentEpoch: Epoch): phase0.Validator[] { + return this.binding.getValidatorsByStatus(statuses, currentEpoch); + } + + get validatorCount(): number { + if (this._validatorCount === null) { + this._validatorCount = this.binding.validatorCount; + } + return this._validatorCount; + } + + get activeValidatorCount(): number { + if (this._activeValidatorCount === null) { + this._activeValidatorCount = this.binding.activeValidatorCount; + } + return this._activeValidatorCount; + } + + getAllValidators(): phase0.Validator[] { + if (this._getAllValidators === null) { + this._getAllValidators = this.binding.getAllValidators(); + } + return this._getAllValidators; + } + + getAllBalances(): number[] { + if (this._getAllBalances === null) { + this._getAllBalances = this.binding.getAllBalances(); + } + return this._getAllBalances; + } + + // API + + get proposerRewards(): RewardCache { + return this.binding.proposerRewards; + } + + computeBlockRewards(block: BeaconBlock, proposerRewards?: RewardCache): Promise { + return this.binding.computeBlockRewards(block, proposerRewards); + } + + computeAttestationsRewards(validatorIds?: (ValidatorIndex | string)[]): Promise { + return this.binding.computeAttestationsRewards(validatorIds); + } + + getLatestWeakSubjectivityCheckpointEpoch(): Epoch { + if (this._getLatestWeakSubjectivityCheckpointEpoch === null) { + this._getLatestWeakSubjectivityCheckpointEpoch = this.binding.getLatestWeakSubjectivityCheckpointEpoch(); + } + return this._getLatestWeakSubjectivityCheckpointEpoch; + } + + // Validation + + getVoluntaryExitValidity( + signedVoluntaryExit: phase0.SignedVoluntaryExit, + verifySignature: boolean + ): VoluntaryExitValidity { + return this.binding.getVoluntaryExitValidity(signedVoluntaryExit, verifySignature); + } + + isValidVoluntaryExit(signedVoluntaryExit: phase0.SignedVoluntaryExit, verifySignature: boolean): boolean { + return this.binding.isValidVoluntaryExit(signedVoluntaryExit, verifySignature); + } + + // Proofs + + getFinalizedRootProof(): Uint8Array[] { + if (this._getFinalizedRootProof === null) { + this._getFinalizedRootProof = this.binding.getFinalizedRootProof(); + } + return this._getFinalizedRootProof; + } + + getSingleProof(gindex: bigint): Uint8Array[] { + let cached = this._getSingleProof.get(gindex); + if (cached === undefined) { + cached = this.binding.getSingleProof(gindex); + this._getSingleProof.set(gindex, cached); + } + return cached; + } + + createMultiProof(descriptor: Uint8Array): CompactMultiProof { + return this.binding.createMultiProof(descriptor); + } + + // Fork choice + + computeUnrealizedCheckpoints(): { + justifiedCheckpoint: phase0.Checkpoint; + finalizedCheckpoint: phase0.Checkpoint; + } { + if (this._computeUnrealizedCheckpoints === null) { + this._computeUnrealizedCheckpoints = this.binding.computeUnrealizedCheckpoints(); + } + return this._computeUnrealizedCheckpoints; + } + + computeAnchorCheckpoint(): {checkpoint: phase0.Checkpoint; blockHeader: phase0.BeaconBlockHeader} { + if (this._computeAnchorCheckpoint === null) { + this._computeAnchorCheckpoint = this.binding.computeAnchorCheckpoint(); + } + return this._computeAnchorCheckpoint; + } + + // Backward compatibility + + get clonedCount(): number { + return this.binding.clonedCount; + } + + get clonedCountWithTransferCache(): number { + return this.binding.clonedCountWithTransferCache; + } + + get createdWithTransferCache(): boolean { + if (this._createdWithTransferCache === null) { + this._createdWithTransferCache = this.binding.createdWithTransferCache; + } + return this._createdWithTransferCache; + } + + isStateValidatorsNodesPopulated(): boolean { + if (this._isStateValidatorsNodesPopulated === null) { + this._isStateValidatorsNodesPopulated = this.binding.isStateValidatorsNodesPopulated(); + } + return this._isStateValidatorsNodesPopulated; + } + + // Serialization + + loadOtherState( + stateBytes: Uint8Array, + seedValidatorsBytes?: Uint8Array, + opts?: {preloadValidatorsAndBalances?: boolean} + ): IBeaconStateView { + return new NativeBeaconStateView(this.binding.loadOtherState(stateBytes, seedValidatorsBytes, opts)); + } + + toValue(): BeaconState { + if (this._toValue === null) { + this._toValue = this.binding.toValue(); + } + return this._toValue; + } + + serialize(): Uint8Array { + if (this._serialize === null) { + this._serialize = this.binding.serialize(); + } + return this._serialize; + } + + serializedSize(): number { + if (this._serializedSize === null) { + this._serializedSize = this.binding.serializedSize(); + } + return this._serializedSize; + } + + serializeToBytes(output: ByteViews, offset: number): number { + return this.binding.serializeToBytes(output, offset); + } + + serializeValidators(): Uint8Array { + if (this._serializeValidators === null) { + this._serializeValidators = this.binding.serializeValidators(); + } + return this._serializeValidators; + } + + serializedValidatorsSize(): number { + if (this._serializedValidatorsSize === null) { + this._serializedValidatorsSize = this.binding.serializedValidatorsSize(); + } + return this._serializedValidatorsSize; + } + + serializeValidatorsToBytes(output: ByteViews, offset: number): number { + return this.binding.serializeValidatorsToBytes(output, offset); + } + + hashTreeRoot(): Uint8Array { + if (this._hashTreeRoot === null) { + this._hashTreeRoot = this.binding.hashTreeRoot(); + } + return this._hashTreeRoot; + } + + // State transition + + stateTransition( + signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock, + options: StateTransitionOpts, + modules: StateTransitionModules + ): IBeaconStateView { + return new NativeBeaconStateView(this.binding.stateTransition(signedBlock, options, modules)); + } + + processSlots( + slot: Slot, + epochTransitionCacheOpts?: EpochTransitionCacheOpts & {dontTransferCache?: boolean}, + modules?: StateTransitionModules + ): IBeaconStateView { + return new NativeBeaconStateView(this.binding.processSlots(slot, epochTransitionCacheOpts, modules)); + } + + // ─── altair ────────────────────────────────────────────────────────────── + + get previousEpochParticipation(): Uint8Array { + if (this._previousEpochParticipation === null) { + this._previousEpochParticipation = this.binding.previousEpochParticipation; + } + return this._previousEpochParticipation; + } + + get currentEpochParticipation(): Uint8Array { + if (this._currentEpochParticipation === null) { + this._currentEpochParticipation = this.binding.currentEpochParticipation; + } + return this._currentEpochParticipation; + } + + getPreviousEpochParticipation(validatorIndex: ValidatorIndex): number { + return this.previousEpochParticipation[validatorIndex]; + } + + getCurrentEpochParticipation(validatorIndex: ValidatorIndex): number { + return this.currentEpochParticipation[validatorIndex]; + } + + get currentSyncCommittee(): altair.SyncCommittee { + if (this._currentSyncCommittee === null) { + this._currentSyncCommittee = this.binding.currentSyncCommittee; + } + return this._currentSyncCommittee; + } + + get nextSyncCommittee(): altair.SyncCommittee { + if (this._nextSyncCommittee === null) { + this._nextSyncCommittee = this.binding.nextSyncCommittee; + } + return this._nextSyncCommittee; + } + + get currentSyncCommitteeIndexed(): SyncCommitteeCache { + if (this._currentSyncCommitteeIndexed === null) { + this._currentSyncCommitteeIndexed = this.binding.currentSyncCommitteeIndexed; + } + return this._currentSyncCommitteeIndexed; + } + + get syncProposerReward(): number { + if (this._syncProposerReward === null) { + this._syncProposerReward = this.binding.syncProposerReward; + } + return this._syncProposerReward; + } + + getIndexedSyncCommitteeAtEpoch(epoch: Epoch): SyncCommitteeCache { + let cached = this._getIndexedSyncCommitteeAtEpoch.get(epoch); + if (cached === undefined) { + cached = this.binding.getIndexedSyncCommitteeAtEpoch(epoch); + this._getIndexedSyncCommitteeAtEpoch.set(epoch, cached); + } + return cached; + } + + getIndexedSyncCommittee(slot: Slot): SyncCommitteeCache { + let cached = this._getIndexedSyncCommittee.get(slot); + if (cached === undefined) { + cached = this.binding.getIndexedSyncCommittee(slot); + this._getIndexedSyncCommittee.set(slot, cached); + } + return cached; + } + + computeSyncCommitteeRewards( + block: BeaconBlock, + validatorIds: (ValidatorIndex | string)[] + ): Promise { + return this.binding.computeSyncCommitteeRewards(block, validatorIds); + } + + getSyncCommitteesWitness(): SyncCommitteeWitness { + if (this._getSyncCommitteesWitness === null) { + this._getSyncCommitteesWitness = this.binding.getSyncCommitteesWitness(); + } + return this._getSyncCommitteesWitness; + } + + // ─── bellatrix ─────────────────────────────────────────────────────────── + + get latestExecutionPayloadHeader(): ExecutionPayloadHeader { + if (this._latestExecutionPayloadHeader === null) { + this._latestExecutionPayloadHeader = this.binding.latestExecutionPayloadHeader; + } + return this._latestExecutionPayloadHeader; + } + + get payloadBlockNumber(): number { + if (this._payloadBlockNumber === null) { + this._payloadBlockNumber = this.binding.payloadBlockNumber; + } + return this._payloadBlockNumber; + } + + get isExecutionStateType(): boolean { + if (this._isExecutionStateType === null) { + this._isExecutionStateType = this.binding.isExecutionStateType; + } + return this._isExecutionStateType; + } + + get isMergeTransitionComplete(): boolean { + if (this._isMergeTransitionComplete === null) { + this._isMergeTransitionComplete = this.binding.isMergeTransitionComplete; + } + return this._isMergeTransitionComplete; + } + + isExecutionEnabled(block: BeaconBlock | BlindedBeaconBlock): boolean { + return this.binding.isExecutionEnabled(block); + } + + // ─── capella ───────────────────────────────────────────────────────────── + + get historicalSummaries(): capella.HistoricalSummaries { + if (this._historicalSummaries === null) { + this._historicalSummaries = this.binding.historicalSummaries; + } + return this._historicalSummaries; + } + + getExpectedWithdrawals(): { + expectedWithdrawals: capella.Withdrawal[]; + processedBuilderWithdrawalsCount: number; + processedPartialWithdrawalsCount: number; + processedBuildersSweepCount: number; + processedValidatorSweepCount: number; + } { + if (this._getExpectedWithdrawals === null) { + this._getExpectedWithdrawals = this.binding.getExpectedWithdrawals(); + } + return this._getExpectedWithdrawals; + } + + // ─── electra ───────────────────────────────────────────────────────────── + + get pendingDeposits(): electra.PendingDeposits { + if (this._pendingDeposits === null) { + this._pendingDeposits = this.binding.pendingDeposits; + } + return this._pendingDeposits; + } + + get pendingDepositsCount(): number { + if (this._pendingDepositsCount === null) { + this._pendingDepositsCount = this.binding.pendingDepositsCount; + } + return this._pendingDepositsCount; + } + + get pendingPartialWithdrawals(): electra.PendingPartialWithdrawals { + if (this._pendingPartialWithdrawals === null) { + this._pendingPartialWithdrawals = this.binding.pendingPartialWithdrawals; + } + return this._pendingPartialWithdrawals; + } + + get pendingPartialWithdrawalsCount(): number { + if (this._pendingPartialWithdrawalsCount === null) { + this._pendingPartialWithdrawalsCount = this.binding.pendingPartialWithdrawalsCount; + } + return this._pendingPartialWithdrawalsCount; + } + + get pendingConsolidations(): electra.PendingConsolidations { + if (this._pendingConsolidations === null) { + this._pendingConsolidations = this.binding.pendingConsolidations; + } + return this._pendingConsolidations; + } + + get pendingConsolidationsCount(): number { + if (this._pendingConsolidationsCount === null) { + this._pendingConsolidationsCount = this.binding.pendingConsolidationsCount; + } + return this._pendingConsolidationsCount; + } + + // ─── fulu ──────────────────────────────────────────────────────────────── + + get proposerLookahead(): fulu.ProposerLookahead { + if (this._proposerLookahead === null) { + this._proposerLookahead = this.binding.proposerLookahead; + } + return this._proposerLookahead; + } + + // ─── gloas ─────────────────────────────────────────────────────────────── + + get latestBlockHash(): Bytes32 { + if (this._latestBlockHash === null) { + this._latestBlockHash = this.binding.latestBlockHash; + } + return this._latestBlockHash; + } + + // executionPayloadAvailability getter is defined near the top of the class. + + get latestExecutionPayloadBid(): ExecutionPayloadBid { + if (this._latestExecutionPayloadBid === null) { + this._latestExecutionPayloadBid = this.binding.latestExecutionPayloadBid; + } + return this._latestExecutionPayloadBid; + } + + get payloadExpectedWithdrawals(): capella.Withdrawal[] { + if (this._payloadExpectedWithdrawals === null) { + this._payloadExpectedWithdrawals = this.binding.payloadExpectedWithdrawals; + } + return this._payloadExpectedWithdrawals; + } + + getBuilder(index: BuilderIndex): gloas.Builder { + let cached = this._getBuilder.get(index); + if (cached === undefined) { + cached = this.binding.getBuilder(index); + this._getBuilder.set(index, cached); + } + return cached; + } + + canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean { + return this.binding.canBuilderCoverBid(builderIndex, bidAmount); + } + + getEpochPTCs(epoch: Epoch): Uint32Array[] { + let cached = this._getEpochPTCs.get(epoch); + if (cached === undefined) { + cached = this.binding.getEpochPTCs(epoch); + this._getEpochPTCs.set(epoch, cached); + } + return cached; + } + + getIndicesInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number[] { + return this.binding.getIndicesInPayloadTimelinessCommittee(validatorIndex, slot); + } + + withParentPayloadApplied(executionRequests: electra.ExecutionRequests): IBeaconStateViewGloas { + const view = new NativeBeaconStateView(this.binding.withParentPayloadApplied(executionRequests)); + if (!isStatePostGloas(view)) { + throw new Error("Expected gloas state from withParentPayloadApplied"); + } + return view; + } +} diff --git a/packages/state-transition/src/stateView/stateViewFactory.ts b/packages/state-transition/src/stateView/stateViewFactory.ts index 122d5cc20364..a7ab2def414f 100644 --- a/packages/state-transition/src/stateView/stateViewFactory.ts +++ b/packages/state-transition/src/stateView/stateViewFactory.ts @@ -31,6 +31,7 @@ type NativeOpts = { export function createBeaconStateView(opts: NodeJSOpts | NativeOpts): IBeaconStateView { if (opts.useNative) { throw new Error("Native (Zig) BeaconStateView not yet implemented"); + // TODO: return a new instance of NativeBeaconStateView } const {anchorState, config, pubkeyCache} = opts; const cachedState = createCachedBeaconState(anchorState, {config, pubkeyCache}, {skipSyncPubkeys: true}); @@ -69,6 +70,7 @@ type RegenNativeOpts = { export function createBeaconStateViewForHistoricalRegen(opts: RegenNodeJSOpts | RegenNativeOpts): IBeaconStateView { if (opts.useNative) { throw new Error("Native (Zig) BeaconStateView not yet implemented"); + // TODO: return a new instance of NativeBeaconStateView } const {config, stateBytes} = opts; const state = getStateTypeFromBytes(config, stateBytes).deserializeToViewDU(stateBytes); diff --git a/packages/state-transition/test/unit/stateView/nativeBeaconStateView.test.ts b/packages/state-transition/test/unit/stateView/nativeBeaconStateView.test.ts new file mode 100644 index 000000000000..f35facad5ad4 --- /dev/null +++ b/packages/state-transition/test/unit/stateView/nativeBeaconStateView.test.ts @@ -0,0 +1,85 @@ +import {describe, expect, it} from "vitest"; +import {BitArray} from "@chainsafe/ssz"; +import {IBeaconStateViewNative} from "../../../src/stateView/interface.js"; +import {NativeBeaconStateView} from "../../../src/stateView/nativeBeaconStateView.js"; + +describe("NativeBeaconStateView", () => { + it("lifts the raw {uint8Array, bitLen} into a BitArray for executionPayloadAvailability", () => { + // 0b10100101 — bits at indices 0, 2, 5, 7 are set + const uint8Array = new Uint8Array([0b10100101]); + const bitLen = 8; + + let fetchCount = 0; + const binding = { + get executionPayloadAvailability() { + fetchCount++; + return {uint8Array, bitLen}; + }, + } as unknown as IBeaconStateViewNative; + + const view = new NativeBeaconStateView(binding); + const bits = view.executionPayloadAvailability; + + expect(bits).toBeInstanceOf(BitArray); + expect(bits.bitLen).toBe(bitLen); + expect(bits.uint8Array).toBe(uint8Array); + expect(bits.get(0)).toBe(true); + expect(bits.get(1)).toBe(false); + expect(bits.get(2)).toBe(true); + expect(bits.get(5)).toBe(true); + expect(bits.get(7)).toBe(true); + + // Cached: a second access doesn't go back to the binding + expect(view.executionPayloadAvailability).toBe(bits); + expect(fetchCount).toBe(1); + }); + + it("caches forwarded properties so the binding is hit once", () => { + let forkAccessCount = 0; + let latestBlockHeaderAccessCount = 0; + const fakeFork = {previousVersion: new Uint8Array(4), currentVersion: new Uint8Array(4), epoch: 0}; + const fakeHeader = { + slot: 0, + proposerIndex: 0, + parentRoot: new Uint8Array(32), + stateRoot: new Uint8Array(32), + bodyRoot: new Uint8Array(32), + }; + + const binding = { + get fork() { + forkAccessCount++; + return fakeFork; + }, + get latestBlockHeader() { + latestBlockHeaderAccessCount++; + return fakeHeader; + }, + } as unknown as IBeaconStateViewNative; + + const view = new NativeBeaconStateView(binding); + expect(view.fork).toBe(fakeFork); + expect(view.fork).toBe(fakeFork); + expect(view.latestBlockHeader).toBe(fakeHeader); + expect(view.latestBlockHeader).toBe(fakeHeader); + expect(forkAccessCount).toBe(1); + expect(latestBlockHeaderAccessCount).toBe(1); + }); + + it("delegates pass-through getters and methods to the binding", () => { + const binding = { + slot: 123, + epoch: 4, + validatorCount: 17, + getBlockRootAtSlot: (slot: number) => new Uint8Array([slot & 0xff]), + getBalance: (index: number) => 32_000_000_000 + index, + } as unknown as IBeaconStateViewNative; + + const view = new NativeBeaconStateView(binding); + expect(view.slot).toBe(123); + expect(view.epoch).toBe(4); + expect(view.validatorCount).toBe(17); + expect(view.getBlockRootAtSlot(7)).toEqual(new Uint8Array([7])); + expect(view.getBalance(2)).toBe(32_000_000_002); + }); +});