Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: load state from Uint8Array #6057

Merged
merged 12 commits into from
Oct 31, 2023
2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
},
"dependencies": {
"@chainsafe/persistent-merkle-tree": "^0.6.1",
"@chainsafe/ssz": "^0.13.0",
"@chainsafe/ssz": "^0.14.0",
"@lodestar/config": "^1.11.3",
"@lodestar/params": "^1.11.3",
"@lodestar/types": "^1.11.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
"@chainsafe/libp2p-noise": "^13.0.1",
"@chainsafe/persistent-merkle-tree": "^0.6.1",
"@chainsafe/prometheus-gc-stats": "^1.0.0",
"@chainsafe/ssz": "^0.13.0",
"@chainsafe/ssz": "^0.14.0",
"@chainsafe/threads": "^1.11.1",
"@ethersproject/abi": "^5.7.0",
"@fastify/bearer-auth": "^9.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"@chainsafe/bls-keystore": "^2.0.0",
"@chainsafe/blst": "^0.2.9",
"@chainsafe/discv5": "^5.1.0",
"@chainsafe/ssz": "^0.13.0",
"@chainsafe/ssz": "^0.14.0",
"@chainsafe/threads": "^1.11.1",
"@libp2p/crypto": "^2.0.4",
"@libp2p/peer-id": "^3.0.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"blockchain"
],
"dependencies": {
"@chainsafe/ssz": "^0.13.0",
"@chainsafe/ssz": "^0.14.0",
"@lodestar/params": "^1.11.3",
"@lodestar/types": "^1.11.3"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"check-readme": "typescript-docs-verifier"
},
"dependencies": {
"@chainsafe/ssz": "^0.13.0",
"@chainsafe/ssz": "^0.14.0",
"@lodestar/config": "^1.11.3",
"@lodestar/utils": "^1.11.3",
"@types/levelup": "^4.3.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/fork-choice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"check-readme": "typescript-docs-verifier"
},
"dependencies": {
"@chainsafe/ssz": "^0.13.0",
"@chainsafe/ssz": "^0.14.0",
"@lodestar/config": "^1.11.3",
"@lodestar/params": "^1.11.3",
"@lodestar/state-transition": "^1.11.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/light-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"dependencies": {
"@chainsafe/bls": "7.1.1",
"@chainsafe/persistent-merkle-tree": "^0.6.1",
"@chainsafe/ssz": "^0.13.0",
"@chainsafe/ssz": "^0.14.0",
"@lodestar/api": "^1.11.3",
"@lodestar/config": "^1.11.3",
"@lodestar/params": "^1.11.3",
Expand Down
3 changes: 2 additions & 1 deletion packages/state-transition/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@
"dependencies": {
"@chainsafe/as-sha256": "^0.3.1",
"@chainsafe/bls": "7.1.1",
"@chainsafe/blst": "^0.2.9",
"@chainsafe/persistent-merkle-tree": "^0.6.1",
"@chainsafe/persistent-ts": "^0.19.1",
"@chainsafe/ssz": "^0.13.0",
"@chainsafe/ssz": "^0.14.0",
"@lodestar/config": "^1.11.3",
"@lodestar/params": "^1.11.3",
"@lodestar/types": "^1.11.3",
Expand Down
28 changes: 18 additions & 10 deletions packages/state-transition/src/cache/epochCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ import {
computeProposers,
getActivationChurnLimit,
} from "../util/index.js";
import {computeEpochShuffling, EpochShuffling} from "../util/epochShuffling.js";
import {computeEpochShuffling, EpochShuffling, getShufflingDecisionBlock} from "../util/epochShuffling.js";
import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js";
import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js";
import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements.js";
import {Index2PubkeyCache, PubkeyIndexMap, syncPubkeys} from "./pubkeyCache.js";
import {BeaconStateAllForks, BeaconStateAltair} from "./types.js";
import {BeaconStateAllForks, BeaconStateAltair, ShufflingGetter} from "./types.js";
import {
computeSyncCommitteeCache,
getSyncCommitteeCache,
Expand All @@ -51,6 +51,7 @@ export type EpochCacheImmutableData = {
export type EpochCacheOpts = {
skipSyncCommitteeCache?: boolean;
skipSyncPubkeys?: boolean;
shufflingGetter?: ShufflingGetter;
};

/** Defers computing proposers by persisting only the seed, and dropping it once indexes are computed */
Expand Down Expand Up @@ -280,21 +281,28 @@ export class EpochCache {
const currentActiveIndices: ValidatorIndex[] = [];
const nextActiveIndices: ValidatorIndex[] = [];

const previousShufflingDecisionBlock = getShufflingDecisionBlock(state, previousEpoch);
const previousShufflingIn = opts?.shufflingGetter?.(previousEpoch, previousShufflingDecisionBlock);
const currentShufflingDecisionBlock = getShufflingDecisionBlock(state, currentEpoch);
const currentShufflingIn = opts?.shufflingGetter?.(currentEpoch, currentShufflingDecisionBlock);
const nextShufflingDecisionBlock = getShufflingDecisionBlock(state, nextEpoch);
const nextShufflingIn = opts?.shufflingGetter?.(nextEpoch, nextShufflingDecisionBlock);
Copy link
Contributor

Choose a reason for hiding this comment

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

What does the In suffix mean? Can you comment above this block detailing the rationale and purpose of these lines?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

those are shufflings comeing from ShufflingCache provided by BeaconChain. Will refactor to cachedNextShuffling and add more comments 👍


for (let i = 0; i < validatorCount; i++) {
const validator = validators[i];

// Note: Not usable for fork-choice balances since in-active validators are not zero'ed
effectiveBalanceIncrements[i] = Math.floor(validator.effectiveBalance / EFFECTIVE_BALANCE_INCREMENT);

if (isActiveValidator(validator, previousEpoch)) {
if (previousShufflingIn == null && isActiveValidator(validator, previousEpoch)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you detail more the motivation of this change?

previousActiveIndices.push(i);
}
if (isActiveValidator(validator, currentEpoch)) {
if (currentShufflingIn == null && isActiveValidator(validator, currentEpoch)) {
currentActiveIndices.push(i);
// We track totalActiveBalanceIncrements as ETH to fit total network balance in a JS number (53 bits)
totalActiveBalanceIncrements += effectiveBalanceIncrements[i];
}
if (isActiveValidator(validator, nextEpoch)) {
if (nextShufflingIn == null && isActiveValidator(validator, nextEpoch)) {
nextActiveIndices.push(i);
}

Expand All @@ -317,11 +325,11 @@ export class EpochCache {
throw Error("totalActiveBalanceIncrements >= Number.MAX_SAFE_INTEGER. MAX_EFFECTIVE_BALANCE is too low.");
}

const currentShuffling = computeEpochShuffling(state, currentActiveIndices, currentEpoch);
const previousShuffling = isGenesis
? currentShuffling
: computeEpochShuffling(state, previousActiveIndices, previousEpoch);
const nextShuffling = computeEpochShuffling(state, nextActiveIndices, nextEpoch);
const currentShuffling = currentShufflingIn ?? computeEpochShuffling(state, currentActiveIndices, currentEpoch);
const previousShuffling =
previousShufflingIn ??
(isGenesis ? currentShuffling : computeEpochShuffling(state, previousActiveIndices, previousEpoch));
const nextShuffling = nextShufflingIn ?? computeEpochShuffling(state, nextActiveIndices, nextEpoch);

const currentProposerSeed = getSeed(state, currentEpoch, DOMAIN_BEACON_PROPOSER);

Expand Down
42 changes: 40 additions & 2 deletions packages/state-transition/src/cache/stateCache.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import bls from "@chainsafe/bls";
import {CoordType} from "@chainsafe/blst";
import {BeaconConfig} from "@lodestar/config";
import {loadState} from "../util/loadState/loadState.js";
import {EpochCache, EpochCacheImmutableData, EpochCacheOpts} from "./epochCache.js";
import {
BeaconStateAllForks,
Expand Down Expand Up @@ -137,13 +140,48 @@ export function createCachedBeaconState<T extends BeaconStateAllForks>(
immutableData: EpochCacheImmutableData,
opts?: EpochCacheOpts
): T & BeaconStateCache {
return getCachedBeaconState(state, {
const epochCache = EpochCache.createFromState(state, immutableData, opts);
const cachedState = getCachedBeaconState(state, {
config: immutableData.config,
epochCtx: EpochCache.createFromState(state, immutableData, opts),
epochCtx: epochCache,
clonedCount: 0,
clonedCountWithTransferCache: 0,
createdWithTransferCache: false,
});

return cachedState;
}

/**
* Create a CachedBeaconState given a cached seed state and state bytes
* This guarantees that the returned state shares the same tree with the seed state
* Check loadState() api for more details
*/
export function loadCachedBeaconState<T extends BeaconStateAllForks & BeaconStateCache>(
cachedSeedState: T,
stateBytes: Uint8Array,
opts?: EpochCacheOpts
): T {
const {state: migratedState, modifiedValidators} = loadState(cachedSeedState.config, cachedSeedState, stateBytes);
const {pubkey2index, index2pubkey} = cachedSeedState.epochCtx;
// Get the validators sub tree once for all the loop
const validators = migratedState.validators;
for (const validatorIndex of modifiedValidators) {
const validator = validators.getReadonly(validatorIndex);
const pubkey = validator.pubkey;
pubkey2index.set(pubkey, validatorIndex);
index2pubkey[validatorIndex] = bls.PublicKey.fromBytes(pubkey, CoordType.jacobian);
}

return createCachedBeaconState(
migratedState,
{
config: cachedSeedState.config,
pubkey2index,
index2pubkey,
},
{...(opts ?? {}), ...{skipSyncPubkeys: true}}
) as T;
}

/**
Expand Down
5 changes: 4 additions & 1 deletion packages/state-transition/src/cache/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {CompositeViewDU} from "@chainsafe/ssz";
import {ssz} from "@lodestar/types";
import {Epoch, RootHex, ssz} from "@lodestar/types";
import {EpochShuffling} from "../util/epochShuffling.js";

export type BeaconStatePhase0 = CompositeViewDU<typeof ssz.phase0.BeaconState>;
export type BeaconStateAltair = CompositeViewDU<typeof ssz.altair.BeaconState>;
Expand All @@ -20,3 +21,5 @@ export type BeaconStateAllForks =
| BeaconStateDeneb;

export type BeaconStateExecutions = BeaconStateBellatrix | BeaconStateCapella | BeaconStateDeneb;

export type ShufflingGetter = (shufflingEpoch: Epoch, dependentRoot: RootHex) => EpochShuffling | null;
1 change: 1 addition & 0 deletions packages/state-transition/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type {
// Main state caches
export {
createCachedBeaconState,
loadCachedBeaconState,
type BeaconStateCache,
isCachedBeaconState,
isStateBalancesNodesPopulated,
Expand Down
10 changes: 9 additions & 1 deletion packages/state-transition/src/util/epochShuffling.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {Epoch, ValidatorIndex} from "@lodestar/types";
import {toHexString} from "@chainsafe/ssz";
import {Epoch, RootHex, ValidatorIndex} from "@lodestar/types";
import {intDiv} from "@lodestar/utils";
import {
DOMAIN_BEACON_ATTESTER,
Expand All @@ -9,6 +10,8 @@ import {
import {BeaconStateAllForks} from "../types.js";
import {getSeed} from "./seed.js";
import {unshuffleList} from "./shuffle.js";
import {computeStartSlotAtEpoch} from "./epoch.js";
import {getBlockRootAtSlot} from "./blockRoot.js";

/**
* Readonly interface for EpochShuffling.
Expand Down Expand Up @@ -95,3 +98,8 @@ export function computeEpochShuffling(
committeesPerSlot,
};
}

export function getShufflingDecisionBlock(state: BeaconStateAllForks, epoch: Epoch): RootHex {
const pivotSlot = computeStartSlotAtEpoch(epoch - 1) - 1;
return toHexString(getBlockRootAtSlot(state, pivotSlot));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// UintNum64 = 8 bytes
export const INACTIVITY_SCORE_SIZE = 8;

/**
* As monitored on mainnet, inactivityScores are not changed much and they are mostly 0
* Using Buffer.compare is the fastest way as noted in `./findModifiedValidators.ts`
* @returns output parameter modifiedValidators: validator indices that are modified
*/
export function findModifiedInactivityScores(
inactivityScoresBytes: Uint8Array,
inactivityScoresBytes2: Uint8Array,
modifiedValidators: number[],
validatorOffset = 0
): void {
if (inactivityScoresBytes.length !== inactivityScoresBytes2.length) {
throw new Error(
"inactivityScoresBytes.length !== inactivityScoresBytes2.length " +
inactivityScoresBytes.length +
" vs " +
inactivityScoresBytes2.length
);
}

if (Buffer.compare(inactivityScoresBytes, inactivityScoresBytes2) === 0) {
return;
}

if (inactivityScoresBytes.length === INACTIVITY_SCORE_SIZE) {
modifiedValidators.push(validatorOffset);
return;
}

const numValidator = Math.floor(inactivityScoresBytes.length / INACTIVITY_SCORE_SIZE);
const halfValidator = Math.floor(numValidator / 2);
findModifiedInactivityScores(
inactivityScoresBytes.subarray(0, halfValidator * INACTIVITY_SCORE_SIZE),
inactivityScoresBytes2.subarray(0, halfValidator * INACTIVITY_SCORE_SIZE),
modifiedValidators,
validatorOffset
);
findModifiedInactivityScores(
inactivityScoresBytes.subarray(halfValidator * INACTIVITY_SCORE_SIZE),
inactivityScoresBytes2.subarray(halfValidator * INACTIVITY_SCORE_SIZE),
modifiedValidators,
validatorOffset + halfValidator
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {VALIDATOR_BYTES_SIZE} from "../sszBytes.js";

/**
* Find modified validators by comparing two validators bytes using Buffer.compare() recursively
* - As noted in packages/state-transition/test/perf/util/loadState/findModifiedValidators.test.ts, serializing validators and compare Uint8Array is the fastest way
* - The performance is quite stable and can afford a lot of difference in validators (the benchmark tested up to 10k but it's not likely we have that difference in mainnet)
* - Also packages/state-transition/test/perf/misc/byteArrayEquals.test.ts shows that Buffer.compare() is very efficient for large Uint8Array
*
* @returns output parameter modifiedValidators: validator indices that are modified
*/
export function findModifiedValidators(
validatorsBytes: Uint8Array,
validatorsBytes2: Uint8Array,
modifiedValidators: number[],
validatorOffset = 0
): void {
if (validatorsBytes.length !== validatorsBytes2.length) {
throw new Error(
"validatorsBytes.length !== validatorsBytes2.length " + validatorsBytes.length + " vs " + validatorsBytes2.length
);
}

if (Buffer.compare(validatorsBytes, validatorsBytes2) === 0) {
return;
}

if (validatorsBytes.length === VALIDATOR_BYTES_SIZE) {
modifiedValidators.push(validatorOffset);
return;
}

const numValidator = Math.floor(validatorsBytes.length / VALIDATOR_BYTES_SIZE);
const halfValidator = Math.floor(numValidator / 2);
findModifiedValidators(
validatorsBytes.subarray(0, halfValidator * VALIDATOR_BYTES_SIZE),
validatorsBytes2.subarray(0, halfValidator * VALIDATOR_BYTES_SIZE),
modifiedValidators,
validatorOffset
);
findModifiedValidators(
validatorsBytes.subarray(halfValidator * VALIDATOR_BYTES_SIZE),
validatorsBytes2.subarray(halfValidator * VALIDATOR_BYTES_SIZE),
modifiedValidators,
validatorOffset + halfValidator
);
}
Loading
Loading