Skip to content

Commit b11fe2d

Browse files
authoredOct 31, 2023
feat: load state from Uint8Array (#6057)
* fix: implement loadState api * chore: benchmark findModifiedValidators() * feat: implement deserializeContainerIgnoreFields() * feat: implement loadValidator() * fix: type the ignoreFields * chore: benchmark loadState() * fix: add '--max-old-space-size=4096' to github workflow * fix: revert default vc count in perf test * Revert "fix: add '--max-old-space-size=4096' to github workflow" This reverts commit a420c54. * chore: rename loadCachedBeaconState to loadUnfinalizedCachedBeaconState * chore: loadValidator - reuse fields as much as possible * chore: more benchmarks to compare Uint8Array
1 parent 8dbef3f commit b11fe2d

34 files changed

+1316
-44
lines changed
 

‎packages/api/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
},
7171
"dependencies": {
7272
"@chainsafe/persistent-merkle-tree": "^0.6.1",
73-
"@chainsafe/ssz": "^0.13.0",
73+
"@chainsafe/ssz": "^0.14.0",
7474
"@lodestar/config": "^1.11.3",
7575
"@lodestar/params": "^1.11.3",
7676
"@lodestar/types": "^1.11.3",

‎packages/beacon-node/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
"@chainsafe/libp2p-noise": "^13.0.1",
105105
"@chainsafe/persistent-merkle-tree": "^0.6.1",
106106
"@chainsafe/prometheus-gc-stats": "^1.0.0",
107-
"@chainsafe/ssz": "^0.13.0",
107+
"@chainsafe/ssz": "^0.14.0",
108108
"@chainsafe/threads": "^1.11.1",
109109
"@ethersproject/abi": "^5.7.0",
110110
"@fastify/bearer-auth": "^9.0.0",

‎packages/cli/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
"@chainsafe/bls-keystore": "^2.0.0",
6060
"@chainsafe/blst": "^0.2.9",
6161
"@chainsafe/discv5": "^5.1.0",
62-
"@chainsafe/ssz": "^0.13.0",
62+
"@chainsafe/ssz": "^0.14.0",
6363
"@chainsafe/threads": "^1.11.1",
6464
"@libp2p/crypto": "^2.0.4",
6565
"@libp2p/peer-id": "^3.0.2",

‎packages/config/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
"blockchain"
6565
],
6666
"dependencies": {
67-
"@chainsafe/ssz": "^0.13.0",
67+
"@chainsafe/ssz": "^0.14.0",
6868
"@lodestar/params": "^1.11.3",
6969
"@lodestar/types": "^1.11.3"
7070
}

‎packages/db/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"check-readme": "typescript-docs-verifier"
3838
},
3939
"dependencies": {
40-
"@chainsafe/ssz": "^0.13.0",
40+
"@chainsafe/ssz": "^0.14.0",
4141
"@lodestar/config": "^1.11.3",
4242
"@lodestar/utils": "^1.11.3",
4343
"@types/levelup": "^4.3.3",

‎packages/fork-choice/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"check-readme": "typescript-docs-verifier"
3939
},
4040
"dependencies": {
41-
"@chainsafe/ssz": "^0.13.0",
41+
"@chainsafe/ssz": "^0.14.0",
4242
"@lodestar/config": "^1.11.3",
4343
"@lodestar/params": "^1.11.3",
4444
"@lodestar/state-transition": "^1.11.3",

‎packages/light-client/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
"dependencies": {
6767
"@chainsafe/bls": "7.1.1",
6868
"@chainsafe/persistent-merkle-tree": "^0.6.1",
69-
"@chainsafe/ssz": "^0.13.0",
69+
"@chainsafe/ssz": "^0.14.0",
7070
"@lodestar/api": "^1.11.3",
7171
"@lodestar/config": "^1.11.3",
7272
"@lodestar/params": "^1.11.3",

‎packages/state-transition/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,10 @@
5959
"dependencies": {
6060
"@chainsafe/as-sha256": "^0.3.1",
6161
"@chainsafe/bls": "7.1.1",
62+
"@chainsafe/blst": "^0.2.9",
6263
"@chainsafe/persistent-merkle-tree": "^0.6.1",
6364
"@chainsafe/persistent-ts": "^0.19.1",
64-
"@chainsafe/ssz": "^0.13.0",
65+
"@chainsafe/ssz": "^0.14.0",
6566
"@lodestar/config": "^1.11.3",
6667
"@lodestar/params": "^1.11.3",
6768
"@lodestar/types": "^1.11.3",

‎packages/state-transition/src/cache/epochCache.ts

+22-10
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,12 @@ import {
2626
computeProposers,
2727
getActivationChurnLimit,
2828
} from "../util/index.js";
29-
import {computeEpochShuffling, EpochShuffling} from "../util/epochShuffling.js";
29+
import {computeEpochShuffling, EpochShuffling, getShufflingDecisionBlock} from "../util/epochShuffling.js";
3030
import {computeBaseRewardPerIncrement, computeSyncParticipantReward} from "../util/syncCommittee.js";
3131
import {sumTargetUnslashedBalanceIncrements} from "../util/targetUnslashedBalance.js";
3232
import {EffectiveBalanceIncrements, getEffectiveBalanceIncrementsWithLen} from "./effectiveBalanceIncrements.js";
3333
import {Index2PubkeyCache, PubkeyIndexMap, syncPubkeys} from "./pubkeyCache.js";
34-
import {BeaconStateAllForks, BeaconStateAltair} from "./types.js";
34+
import {BeaconStateAllForks, BeaconStateAltair, ShufflingGetter} from "./types.js";
3535
import {
3636
computeSyncCommitteeCache,
3737
getSyncCommitteeCache,
@@ -51,6 +51,7 @@ export type EpochCacheImmutableData = {
5151
export type EpochCacheOpts = {
5252
skipSyncCommitteeCache?: boolean;
5353
skipSyncPubkeys?: boolean;
54+
shufflingGetter?: ShufflingGetter;
5455
};
5556

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

284+
// BeaconChain could provide a shuffling cache to avoid re-computing shuffling every epoch
285+
// in that case, we don't need to compute shufflings again
286+
const previousShufflingDecisionBlock = getShufflingDecisionBlock(state, previousEpoch);
287+
const cachedPreviousShuffling = opts?.shufflingGetter?.(previousEpoch, previousShufflingDecisionBlock);
288+
const currentShufflingDecisionBlock = getShufflingDecisionBlock(state, currentEpoch);
289+
const cachedCurrentShuffling = opts?.shufflingGetter?.(currentEpoch, currentShufflingDecisionBlock);
290+
const nextShufflingDecisionBlock = getShufflingDecisionBlock(state, nextEpoch);
291+
const cachedNextShuffling = opts?.shufflingGetter?.(nextEpoch, nextShufflingDecisionBlock);
292+
283293
for (let i = 0; i < validatorCount; i++) {
284294
const validator = validators[i];
285295

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

289-
if (isActiveValidator(validator, previousEpoch)) {
299+
// we only need to track active indices for previous, current and next epoch if we have to compute shufflings
300+
// skip doing that if we already have cached shufflings
301+
if (cachedPreviousShuffling == null && isActiveValidator(validator, previousEpoch)) {
290302
previousActiveIndices.push(i);
291303
}
292-
if (isActiveValidator(validator, currentEpoch)) {
304+
if (cachedCurrentShuffling == null && isActiveValidator(validator, currentEpoch)) {
293305
currentActiveIndices.push(i);
294306
// We track totalActiveBalanceIncrements as ETH to fit total network balance in a JS number (53 bits)
295307
totalActiveBalanceIncrements += effectiveBalanceIncrements[i];
296308
}
297-
if (isActiveValidator(validator, nextEpoch)) {
309+
if (cachedNextShuffling == null && isActiveValidator(validator, nextEpoch)) {
298310
nextActiveIndices.push(i);
299311
}
300312

@@ -317,11 +329,11 @@ export class EpochCache {
317329
throw Error("totalActiveBalanceIncrements >= Number.MAX_SAFE_INTEGER. MAX_EFFECTIVE_BALANCE is too low.");
318330
}
319331

320-
const currentShuffling = computeEpochShuffling(state, currentActiveIndices, currentEpoch);
321-
const previousShuffling = isGenesis
322-
? currentShuffling
323-
: computeEpochShuffling(state, previousActiveIndices, previousEpoch);
324-
const nextShuffling = computeEpochShuffling(state, nextActiveIndices, nextEpoch);
332+
const currentShuffling = cachedCurrentShuffling ?? computeEpochShuffling(state, currentActiveIndices, currentEpoch);
333+
const previousShuffling =
334+
cachedPreviousShuffling ??
335+
(isGenesis ? currentShuffling : computeEpochShuffling(state, previousActiveIndices, previousEpoch));
336+
const nextShuffling = cachedNextShuffling ?? computeEpochShuffling(state, nextActiveIndices, nextEpoch);
325337

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

‎packages/state-transition/src/cache/stateCache.ts

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import bls from "@chainsafe/bls";
2+
import {CoordType} from "@chainsafe/blst";
13
import {BeaconConfig} from "@lodestar/config";
4+
import {loadState} from "../util/loadState/loadState.js";
25
import {EpochCache, EpochCacheImmutableData, EpochCacheOpts} from "./epochCache.js";
36
import {
47
BeaconStateAllForks,
@@ -137,13 +140,49 @@ export function createCachedBeaconState<T extends BeaconStateAllForks>(
137140
immutableData: EpochCacheImmutableData,
138141
opts?: EpochCacheOpts
139142
): T & BeaconStateCache {
140-
return getCachedBeaconState(state, {
143+
const epochCache = EpochCache.createFromState(state, immutableData, opts);
144+
const cachedState = getCachedBeaconState(state, {
141145
config: immutableData.config,
142-
epochCtx: EpochCache.createFromState(state, immutableData, opts),
146+
epochCtx: epochCache,
143147
clonedCount: 0,
144148
clonedCountWithTransferCache: 0,
145149
createdWithTransferCache: false,
146150
});
151+
152+
return cachedState;
153+
}
154+
155+
/**
156+
* Create a CachedBeaconState given a cached seed state and state bytes
157+
* This guarantees that the returned state shares the same tree with the seed state
158+
* Check loadState() api for more details
159+
* TODO: after EIP-6110 need to provide a pivotValidatorIndex to decide which comes to finalized validators cache, which comes to unfinalized cache
160+
*/
161+
export function loadUnfinalizedCachedBeaconState<T extends BeaconStateAllForks & BeaconStateCache>(
162+
cachedSeedState: T,
163+
stateBytes: Uint8Array,
164+
opts?: EpochCacheOpts
165+
): T {
166+
const {state: migratedState, modifiedValidators} = loadState(cachedSeedState.config, cachedSeedState, stateBytes);
167+
const {pubkey2index, index2pubkey} = cachedSeedState.epochCtx;
168+
// Get the validators sub tree once for all the loop
169+
const validators = migratedState.validators;
170+
for (const validatorIndex of modifiedValidators) {
171+
const validator = validators.getReadonly(validatorIndex);
172+
const pubkey = validator.pubkey;
173+
pubkey2index.set(pubkey, validatorIndex);
174+
index2pubkey[validatorIndex] = bls.PublicKey.fromBytes(pubkey, CoordType.jacobian);
175+
}
176+
177+
return createCachedBeaconState(
178+
migratedState,
179+
{
180+
config: cachedSeedState.config,
181+
pubkey2index,
182+
index2pubkey,
183+
},
184+
{...(opts ?? {}), ...{skipSyncPubkeys: true}}
185+
) as T;
147186
}
148187

149188
/**

‎packages/state-transition/src/cache/types.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {CompositeViewDU} from "@chainsafe/ssz";
2-
import {ssz} from "@lodestar/types";
2+
import {Epoch, RootHex, ssz} from "@lodestar/types";
3+
import {EpochShuffling} from "../util/epochShuffling.js";
34

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

2223
export type BeaconStateExecutions = BeaconStateBellatrix | BeaconStateCapella | BeaconStateDeneb;
24+
25+
export type ShufflingGetter = (shufflingEpoch: Epoch, dependentRoot: RootHex) => EpochShuffling | null;

‎packages/state-transition/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type {
2525
// Main state caches
2626
export {
2727
createCachedBeaconState,
28+
loadUnfinalizedCachedBeaconState,
2829
type BeaconStateCache,
2930
isCachedBeaconState,
3031
isStateBalancesNodesPopulated,

‎packages/state-transition/src/util/epochShuffling.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {Epoch, ValidatorIndex} from "@lodestar/types";
1+
import {toHexString} from "@chainsafe/ssz";
2+
import {Epoch, RootHex, ValidatorIndex} from "@lodestar/types";
23
import {intDiv} from "@lodestar/utils";
34
import {
45
DOMAIN_BEACON_ATTESTER,
@@ -9,6 +10,8 @@ import {
910
import {BeaconStateAllForks} from "../types.js";
1011
import {getSeed} from "./seed.js";
1112
import {unshuffleList} from "./shuffle.js";
13+
import {computeStartSlotAtEpoch} from "./epoch.js";
14+
import {getBlockRootAtSlot} from "./blockRoot.js";
1215

1316
/**
1417
* Readonly interface for EpochShuffling.
@@ -95,3 +98,8 @@ export function computeEpochShuffling(
9598
committeesPerSlot,
9699
};
97100
}
101+
102+
export function getShufflingDecisionBlock(state: BeaconStateAllForks, epoch: Epoch): RootHex {
103+
const pivotSlot = computeStartSlotAtEpoch(epoch - 1) - 1;
104+
return toHexString(getBlockRootAtSlot(state, pivotSlot));
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// UintNum64 = 8 bytes
2+
export const INACTIVITY_SCORE_SIZE = 8;
3+
4+
/**
5+
* As monitored on mainnet, inactivityScores are not changed much and they are mostly 0
6+
* Using Buffer.compare is the fastest way as noted in `./findModifiedValidators.ts`
7+
* @returns output parameter modifiedValidators: validator indices that are modified
8+
*/
9+
export function findModifiedInactivityScores(
10+
inactivityScoresBytes: Uint8Array,
11+
inactivityScoresBytes2: Uint8Array,
12+
modifiedValidators: number[],
13+
validatorOffset = 0
14+
): void {
15+
if (inactivityScoresBytes.length !== inactivityScoresBytes2.length) {
16+
throw new Error(
17+
"inactivityScoresBytes.length !== inactivityScoresBytes2.length " +
18+
inactivityScoresBytes.length +
19+
" vs " +
20+
inactivityScoresBytes2.length
21+
);
22+
}
23+
24+
if (Buffer.compare(inactivityScoresBytes, inactivityScoresBytes2) === 0) {
25+
return;
26+
}
27+
28+
if (inactivityScoresBytes.length === INACTIVITY_SCORE_SIZE) {
29+
modifiedValidators.push(validatorOffset);
30+
return;
31+
}
32+
33+
const numValidator = Math.floor(inactivityScoresBytes.length / INACTIVITY_SCORE_SIZE);
34+
const halfValidator = Math.floor(numValidator / 2);
35+
findModifiedInactivityScores(
36+
inactivityScoresBytes.subarray(0, halfValidator * INACTIVITY_SCORE_SIZE),
37+
inactivityScoresBytes2.subarray(0, halfValidator * INACTIVITY_SCORE_SIZE),
38+
modifiedValidators,
39+
validatorOffset
40+
);
41+
findModifiedInactivityScores(
42+
inactivityScoresBytes.subarray(halfValidator * INACTIVITY_SCORE_SIZE),
43+
inactivityScoresBytes2.subarray(halfValidator * INACTIVITY_SCORE_SIZE),
44+
modifiedValidators,
45+
validatorOffset + halfValidator
46+
);
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {VALIDATOR_BYTES_SIZE} from "../sszBytes.js";
2+
3+
/**
4+
* Find modified validators by comparing two validators bytes using Buffer.compare() recursively
5+
* - As noted in packages/state-transition/test/perf/util/loadState/findModifiedValidators.test.ts, serializing validators and compare Uint8Array is the fastest way
6+
* - 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)
7+
* - Also packages/state-transition/test/perf/misc/byteArrayEquals.test.ts shows that Buffer.compare() is very efficient for large Uint8Array
8+
*
9+
* @returns output parameter modifiedValidators: validator indices that are modified
10+
*/
11+
export function findModifiedValidators(
12+
validatorsBytes: Uint8Array,
13+
validatorsBytes2: Uint8Array,
14+
modifiedValidators: number[],
15+
validatorOffset = 0
16+
): void {
17+
if (validatorsBytes.length !== validatorsBytes2.length) {
18+
throw new Error(
19+
"validatorsBytes.length !== validatorsBytes2.length " + validatorsBytes.length + " vs " + validatorsBytes2.length
20+
);
21+
}
22+
23+
if (Buffer.compare(validatorsBytes, validatorsBytes2) === 0) {
24+
return;
25+
}
26+
27+
if (validatorsBytes.length === VALIDATOR_BYTES_SIZE) {
28+
modifiedValidators.push(validatorOffset);
29+
return;
30+
}
31+
32+
const numValidator = Math.floor(validatorsBytes.length / VALIDATOR_BYTES_SIZE);
33+
const halfValidator = Math.floor(numValidator / 2);
34+
findModifiedValidators(
35+
validatorsBytes.subarray(0, halfValidator * VALIDATOR_BYTES_SIZE),
36+
validatorsBytes2.subarray(0, halfValidator * VALIDATOR_BYTES_SIZE),
37+
modifiedValidators,
38+
validatorOffset
39+
);
40+
findModifiedValidators(
41+
validatorsBytes.subarray(halfValidator * VALIDATOR_BYTES_SIZE),
42+
validatorsBytes2.subarray(halfValidator * VALIDATOR_BYTES_SIZE),
43+
modifiedValidators,
44+
validatorOffset + halfValidator
45+
);
46+
}

0 commit comments

Comments
 (0)