From 9107456a2763fd37fe2ce9bcc74a5beff5fea8c3 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 1 Jul 2024 09:25:42 +0700 Subject: [PATCH 01/22] feat: consume local ssz batch hash branch --- packages/api/package.json | 4 +- packages/beacon-node/package.json | 7 +- .../src/db/repositories/depositDataRoot.ts | 1 + .../beacon-node/src/eth1/utils/deposits.ts | 1 + .../beacon-node/src/eth1/utils/eth1Data.ts | 1 + packages/cli/package.json | 4 +- packages/cli/src/applyPreset.ts | 2 +- .../cli/src/cmds/beacon/initBeaconState.ts | 4 + packages/cli/src/networks/index.ts | 6 +- packages/config/package.json | 2 +- packages/db/package.json | 2 +- packages/fork-choice/package.json | 2 +- packages/light-client/package.json | 7 +- packages/prover/src/cli/applyPreset.ts | 2 +- packages/state-transition/package.json | 6 +- .../epoch/processParticipationFlagUpdates.ts | 3 +- .../src/util/loadState/loadState.ts | 2 + packages/types/package.json | 4 +- packages/types/src/phase0/listValidator.ts | 14 + packages/types/src/phase0/sszTypes.ts | 3 +- packages/types/src/phase0/validator.ts | 6 + .../types/src/phase0/viewDU/listValidator.ts | 136 +++++++++ packages/types/src/phase0/viewDU/validator.ts | 284 ++++++++++++++++++ packages/utils/package.json | 2 +- packages/validator/package.json | 2 +- yarn.lock | 52 +++- 26 files changed, 519 insertions(+), 40 deletions(-) create mode 100644 packages/types/src/phase0/listValidator.ts create mode 100644 packages/types/src/phase0/viewDU/listValidator.ts create mode 100644 packages/types/src/phase0/viewDU/validator.ts diff --git a/packages/api/package.json b/packages/api/package.json index 5b91b46c128e..3958d177b9b3 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -70,8 +70,8 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/persistent-merkle-tree": "^0.7.1", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@lodestar/config": "^1.20.2", "@lodestar/params": "^1.20.2", "@lodestar/types": "^1.20.2", diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index cc12fa961923..dbfc700bf7b7 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -94,16 +94,17 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/as-sha256": "^0.4.1", "@chainsafe/blst": "^2.0.3", + "@chainsafe/as-sha256": "file:../../../ssz/packages/as-sha256", + "@chainsafe/bls": "7.1.3", "@chainsafe/discv5": "^9.0.0", "@chainsafe/enr": "^3.0.0", "@chainsafe/libp2p-gossipsub": "^13.0.0", "@chainsafe/libp2p-identify": "^1.0.0", "@chainsafe/libp2p-noise": "^15.0.0", - "@chainsafe/persistent-merkle-tree": "^0.7.1", + "@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree", "@chainsafe/prometheus-gc-stats": "^1.0.0", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@chainsafe/threads": "^1.11.1", "@ethersproject/abi": "^5.7.0", "@fastify/bearer-auth": "^9.0.0", diff --git a/packages/beacon-node/src/db/repositories/depositDataRoot.ts b/packages/beacon-node/src/db/repositories/depositDataRoot.ts index fa8983f0e5fa..e0853680097c 100644 --- a/packages/beacon-node/src/db/repositories/depositDataRoot.ts +++ b/packages/beacon-node/src/db/repositories/depositDataRoot.ts @@ -61,6 +61,7 @@ export class DepositDataRootRepository extends Repository { async getDepositRootTreeAtIndex(depositIndex: number): Promise { const depositRootTree = await this.getDepositRootTree(); + depositRootTree.commit(); return depositRootTree.sliceTo(depositIndex); } diff --git a/packages/beacon-node/src/eth1/utils/deposits.ts b/packages/beacon-node/src/eth1/utils/deposits.ts index 19544917ffdc..42ca90f10863 100644 --- a/packages/beacon-node/src/eth1/utils/deposits.ts +++ b/packages/beacon-node/src/eth1/utils/deposits.ts @@ -44,6 +44,7 @@ export function getDepositsWithProofs( eth1Data: phase0.Eth1Data ): phase0.Deposit[] { // Get tree at this particular depositCount to compute correct proofs + depositRootTree.commit(); const viewAtDepositCount = depositRootTree.sliceTo(eth1Data.depositCount - 1); const depositRoot = viewAtDepositCount.hashTreeRoot(); diff --git a/packages/beacon-node/src/eth1/utils/eth1Data.ts b/packages/beacon-node/src/eth1/utils/eth1Data.ts index 2b8e976cac2e..c4244668503f 100644 --- a/packages/beacon-node/src/eth1/utils/eth1Data.ts +++ b/packages/beacon-node/src/eth1/utils/eth1Data.ts @@ -93,6 +93,7 @@ export function getDepositRootByDepositCount(depositCounts: number[], depositRoo const depositRootByDepositCount = new Map(); for (const depositCount of depositCounts) { + depositRootTree.commit(); depositRootTree = depositRootTree.sliceTo(depositCount - 1); depositRootByDepositCount.set(depositCount, depositRootTree.hashTreeRoot()); } diff --git a/packages/cli/package.json b/packages/cli/package.json index 6b7bf36bacd2..2025224c5c17 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -56,8 +56,8 @@ "@chainsafe/blst": "^2.0.3", "@chainsafe/discv5": "^9.0.0", "@chainsafe/enr": "^3.0.0", - "@chainsafe/persistent-merkle-tree": "^0.7.1", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@chainsafe/threads": "^1.11.1", "@libp2p/crypto": "^4.1.0", "@libp2p/peer-id": "^4.1.0", diff --git a/packages/cli/src/applyPreset.ts b/packages/cli/src/applyPreset.ts index 25f78b7d32ac..0d71b68ab78a 100644 --- a/packages/cli/src/applyPreset.ts +++ b/packages/cli/src/applyPreset.ts @@ -1,7 +1,7 @@ // MUST import this file first before anything and not import any Lodestar code. // eslint-disable-next-line no-restricted-imports -import {hasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/as-sha256.js"; +import {hasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/hashtree.js"; // eslint-disable-next-line no-restricted-imports import {setHasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/index.js"; diff --git a/packages/cli/src/cmds/beacon/initBeaconState.ts b/packages/cli/src/cmds/beacon/initBeaconState.ts index c8c444778991..da3ac36d5691 100644 --- a/packages/cli/src/cmds/beacon/initBeaconState.ts +++ b/packages/cli/src/cmds/beacon/initBeaconState.ts @@ -154,6 +154,8 @@ export async function initBeaconState( if (genesisStateFile && !args.forceGenesis) { const stateBytes = await downloadOrLoadFile(genesisStateFile); let anchorState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes); + // not possible to hash the full tree in batch, use the old way to create and drop validator tree one by one + anchorState.node.root; const config = createBeaconConfig(chainForkConfig, anchorState.genesisValidatorsRoot); const wssCheck = isWithinWeakSubjectivityPeriod(config, anchorState, getCheckpointFromState(anchorState)); anchorState = await initStateFromAnchorState(config, db, logger, anchorState, { @@ -183,6 +185,8 @@ async function readWSState( const stateBytes = await downloadOrLoadFile(checkpointState); const wsState = getStateTypeFromBytes(chainForkConfig, stateBytes).deserializeToViewDU(stateBytes); + // not possible to hash the full tree in batch, use the old way to create and drop validator tree one by one + wsState.node.root; const config = createBeaconConfig(chainForkConfig, wsState.genesisValidatorsRoot); const store = lastDbState ?? wsState; const checkpoint = wssCheckpoint ? getCheckpointFromArg(wssCheckpoint) : getCheckpointFromState(wsState); diff --git a/packages/cli/src/networks/index.ts b/packages/cli/src/networks/index.ts index 2d605335b0e8..b929bc0af2c3 100644 --- a/packages/cli/src/networks/index.ts +++ b/packages/cli/src/networks/index.ts @@ -7,7 +7,7 @@ import {getStateTypeFromBytes} from "@lodestar/beacon-node"; import {ChainConfig, ChainForkConfig} from "@lodestar/config"; import {Checkpoint} from "@lodestar/types/phase0"; import {Slot} from "@lodestar/types"; -import {fromHex, callFnWhenAwait, Logger} from "@lodestar/utils"; +import {fromHex, callFnWhenAwait, Logger, toHex} from "@lodestar/utils"; import {BeaconStateAllForks, getLatestBlockRoot, computeCheckpointEpochAtStateSlot} from "@lodestar/state-transition"; import {parseBootnodesFile} from "../util/format.js"; import * as mainnet from "./mainnet.js"; @@ -177,10 +177,12 @@ export async function fetchWeakSubjectivityState( return res.ssz(); }); - logger.info("Download completed", {stateId}); // It should not be required to get fork type from bytes but Checkpointz does not return // Eth-Consensus-Version header, see https://github.com/ethpandaops/checkpointz/issues/164 const wsState = getStateTypeFromBytes(config, stateBytes).deserializeToViewDU(stateBytes); + // not possible to hash the full tree in batch, use the old way to create and drop validator tree one by one + // so use wsState.node.root instead of hashTreeRoot() + logger.info("Download completed", {stateId, root: toHex(wsState.node.root)}); return { wsState, diff --git a/packages/config/package.json b/packages/config/package.json index 8d5fd3d80c35..6752bb960c37 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -64,7 +64,7 @@ "blockchain" ], "dependencies": { - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@lodestar/params": "^1.20.2", "@lodestar/types": "^1.20.2" } diff --git a/packages/db/package.json b/packages/db/package.json index 6cfaf6ecce70..c3190750279b 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -35,7 +35,7 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@lodestar/config": "^1.20.2", "@lodestar/utils": "^1.20.2", "classic-level": "^1.4.1", diff --git a/packages/fork-choice/package.json b/packages/fork-choice/package.json index 2da624af4581..7db358d4cd6d 100644 --- a/packages/fork-choice/package.json +++ b/packages/fork-choice/package.json @@ -36,7 +36,7 @@ "check-readme": "typescript-docs-verifier" }, "dependencies": { - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@lodestar/config": "^1.20.2", "@lodestar/params": "^1.20.2", "@lodestar/state-transition": "^1.20.2", diff --git a/packages/light-client/package.json b/packages/light-client/package.json index 6576bddfee6a..bf49935105e5 100644 --- a/packages/light-client/package.json +++ b/packages/light-client/package.json @@ -74,9 +74,8 @@ }, "dependencies": { "@chainsafe/bls": "7.1.3", - "@chainsafe/blst": "^0.2.0", - "@chainsafe/persistent-merkle-tree": "^0.7.1", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@lodestar/api": "^1.20.2", "@lodestar/config": "^1.20.2", "@lodestar/params": "^1.20.2", @@ -85,7 +84,7 @@ "mitt": "^3.0.0" }, "devDependencies": { - "@chainsafe/as-sha256": "^0.4.1", + "@chainsafe/as-sha256": "file:../../../ssz/packages/as-sha256", "@types/qs": "^6.9.7", "fastify": "^4.27.0", "qs": "^6.11.1", diff --git a/packages/prover/src/cli/applyPreset.ts b/packages/prover/src/cli/applyPreset.ts index 158e05243ec7..73bf9231ec61 100644 --- a/packages/prover/src/cli/applyPreset.ts +++ b/packages/prover/src/cli/applyPreset.ts @@ -1,7 +1,7 @@ // MUST import this file first before anything and not import any Lodestar code. // eslint-disable-next-line no-restricted-imports, import/no-extraneous-dependencies -import {hasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/as-sha256.js"; +import {hasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/hashtree.js"; // eslint-disable-next-line no-restricted-imports, import/no-extraneous-dependencies import {setHasher} from "@chainsafe/persistent-merkle-tree/lib/hasher/index.js"; diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 0271f64b684f..b538876158fa 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -58,11 +58,11 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@chainsafe/as-sha256": "^0.4.1", + "@chainsafe/as-sha256": "file:../../../ssz/packages/as-sha256", "@chainsafe/blst": "^2.0.3", - "@chainsafe/persistent-merkle-tree": "^0.7.1", + "@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree", "@chainsafe/persistent-ts": "^0.19.1", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@lodestar/config": "^1.20.2", "@lodestar/params": "^1.20.2", "@lodestar/types": "^1.20.2", diff --git a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts index fad5857b3ce6..3879d8ef8446 100644 --- a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts +++ b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts @@ -20,7 +20,8 @@ export function processParticipationFlagUpdates(state: CachedBeaconStateAltair): const currentEpochParticipationNode = ssz.altair.EpochParticipation.tree_setChunksNode( state.currentEpochParticipation.node, zeroNode(ssz.altair.EpochParticipation.chunkDepth), - state.currentEpochParticipation.length + state.currentEpochParticipation.length, + null ); state.currentEpochParticipation = ssz.altair.EpochParticipation.getViewDU(currentEpochParticipationNode); diff --git a/packages/state-transition/src/util/loadState/loadState.ts b/packages/state-transition/src/util/loadState/loadState.ts index dc9f8fe4fcab..31773e152ecb 100644 --- a/packages/state-transition/src/util/loadState/loadState.ts +++ b/packages/state-transition/src/util/loadState/loadState.ts @@ -124,6 +124,7 @@ function loadInactivityScores( if (newValidator - 1 < 0) { migratedState.inactivityScores = ssz.altair.InactivityScores.defaultViewDU(); } else { + migratedState.inactivityScores.commit(); migratedState.inactivityScores = migratedState.inactivityScores.sliceTo(newValidator - 1); } } @@ -200,6 +201,7 @@ function loadValidators( modifiedValidators.push(validatorIndex); } } else { + migratedState.validators.commit(); migratedState.validators = migratedState.validators.sliceTo(newValidatorCount - 1); } return modifiedValidators; diff --git a/packages/types/package.json b/packages/types/package.json index d5512763b197..7ab4daa6ba6d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -70,7 +70,9 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", + "@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree", + "@chainsafe/as-sha256": "file:../../../ssz/packages/as-sha256", "@lodestar/params": "^1.20.2", "ethereum-cryptography": "^2.0.0" }, diff --git a/packages/types/src/phase0/listValidator.ts b/packages/types/src/phase0/listValidator.ts new file mode 100644 index 000000000000..a2228d5ec410 --- /dev/null +++ b/packages/types/src/phase0/listValidator.ts @@ -0,0 +1,14 @@ +import {ListCompositeType, ListCompositeTreeViewDU} from "@chainsafe/ssz"; +import {Node} from "@chainsafe/persistent-merkle-tree"; +import {ValidatorNodeStructType} from "./validator.js"; +import {ListValidatorTreeViewDU} from "./viewDU/listValidator.js"; + +export class ListValidatorType extends ListCompositeType { + constructor(limit: number) { + super(new ValidatorNodeStructType(), limit); + } + + getViewDU(node: Node, cache?: unknown): ListCompositeTreeViewDU { + return new ListValidatorTreeViewDU(this, node, cache as any); + } +} diff --git a/packages/types/src/phase0/sszTypes.ts b/packages/types/src/phase0/sszTypes.ts index 9eb2a13e5fae..77dd9d830b15 100644 --- a/packages/types/src/phase0/sszTypes.ts +++ b/packages/types/src/phase0/sszTypes.ts @@ -29,6 +29,7 @@ import { } from "@lodestar/params"; import * as primitiveSsz from "../primitive/sszTypes.js"; import {ValidatorNodeStruct} from "./validator.js"; +import {ListValidatorType} from "./listValidator.js"; const { Bytes32, @@ -228,7 +229,7 @@ export const HistoricalBatchRoots = new ContainerType( export const Validator = ValidatorNodeStruct; // Export as stand-alone for direct tree optimizations -export const Validators = new ListCompositeType(ValidatorNodeStruct, VALIDATOR_REGISTRY_LIMIT); +export const Validators = new ListValidatorType(VALIDATOR_REGISTRY_LIMIT); // this ListUintNum64Type is used to cache Leaf Nodes of BeaconState.balances after epoch transition export const Balances = new ListUintNum64Type(VALIDATOR_REGISTRY_LIMIT); export const RandaoMixes = new VectorCompositeType(Bytes32, EPOCHS_PER_HISTORICAL_VECTOR); diff --git a/packages/types/src/phase0/validator.ts b/packages/types/src/phase0/validator.ts index 3c2f72aac509..5768ed9d36e3 100644 --- a/packages/types/src/phase0/validator.ts +++ b/packages/types/src/phase0/validator.ts @@ -1,5 +1,7 @@ import {ByteViews, ContainerNodeStructType, ValueOfFields} from "@chainsafe/ssz"; +import {Node} from "@chainsafe/persistent-merkle-tree"; import * as primitiveSsz from "../primitive/sszTypes.js"; +import {ValidatorTreeViewDU} from "./viewDU/validator.js"; const {Boolean, Bytes32, UintNum64, BLSPubkey, EpochInf} = primitiveSsz; @@ -33,6 +35,10 @@ export class ValidatorNodeStructType extends ContainerNodeStructType { + + constructor( + readonly type: ListCompositeType, + protected _rootNode: Node, + cache?: ArrayCompositeTreeViewDUCache + ) { + super(type, _rootNode, cache); + // each level 3 of validator has 8 chunks, each chunk has 32 bytes + } + + commit(hashComps: HashComputationGroup | null = null): void { + const isOldRootHashed = this._rootNode.h0 !== null; + if (this.viewsChanged.size === 0) { + if (!isOldRootHashed && hashComps !== null) { + // not possible to get HashComputations due to BranchNodeStruct + this._rootNode.root; + } + return; + } + + // TODO - batch: remove this type cast + const indicesChanged = Array.from(this.viewsChanged.keys()).sort((a, b) => a - b); + const endBatch = indicesChanged.length - (indicesChanged.length % PARALLEL_FACTOR); + // nodesChanged is sorted by index + const nodesChanged: {index: number; node: Node}[] = []; + // commit every 16 validators in batch + for (let i = 0; i < endBatch; i++) { + const indexInBatch = i % PARALLEL_FACTOR; + const viewIndex = indicesChanged[i]; + const viewChanged = this.viewsChanged.get(viewIndex) as ValidatorTreeViewDU; + viewChanged.valueToMerkleBytes(level3ByteViewsArr[indexInBatch], level4BytesArr[indexInBatch]); + + if (indexInBatch === PARALLEL_FACTOR - 1) { + // hash level 4 + const pubkeyRoots = digestNLevelUnsafe(batchLevel4Bytes, 1); + if (pubkeyRoots.length !== PARALLEL_FACTOR * 32) { + throw new Error(`Invalid pubkeyRoots length, expect ${PARALLEL_FACTOR * 32}, got ${pubkeyRoots.length}`); + } + for (let j = 0; j < PARALLEL_FACTOR; j++) { + level3ByteViewsArr[j].uint8Array.set(pubkeyRoots.subarray(j * 32, (j + 1) * 32), 0); + } + const validatorRoots = digestNLevelUnsafe(batchLevel3Bytes, 3); + if (validatorRoots.length !== PARALLEL_FACTOR * 32) { + throw new Error(`Invalid validatorRoots length, expect ${PARALLEL_FACTOR * 32}, got ${validatorRoots.length}`); + } + // commit all validators in this batch + for (let j = PARALLEL_FACTOR - 1; j >= 0; j--) { + const viewIndex = indicesChanged[i - j]; + const indexInBatch = (i - j) % PARALLEL_FACTOR; + const validatorRoot = validatorRoots.subarray(indexInBatch * 32, (indexInBatch + 1) * 32); + const viewChanged = this.viewsChanged.get(viewIndex) as ValidatorTreeViewDU; + viewChanged.commitToRoot(validatorRoot); + nodesChanged.push({index: viewIndex, node: viewChanged.node}); + // Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal + this.nodes[viewIndex] = viewChanged.node; + } + } + } + + // commit the remaining validators, we can do in batch too but don't want to create new Uint8Array views + // it's not much different to commit one by one + for (let i = endBatch; i < indicesChanged.length; i++) { + const viewIndex = indicesChanged[i]; + const viewChanged = this.viewsChanged.get(viewIndex) as ValidatorTreeViewDU; + viewChanged.commit(); + nodesChanged.push({index: viewIndex, node: viewChanged.node}); + // Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal + this.nodes[viewIndex] = viewChanged.node; + } + + // do the remaining commit step the same to parent (ArrayCompositeTreeViewDU) + const indexes = nodesChanged.map((entry) => entry.index); + const nodes = nodesChanged.map((entry) => entry.node); + const chunksNode = this.type.tree_getChunksNode(this._rootNode); + const hashCompsThis = + hashComps != null && isOldRootHashed + ? { + byLevel: hashComps.byLevel, + offset: hashComps.offset + this.type.tree_chunksNodeOffset(), + } + : null; + const newChunksNode = setNodesAtDepth(chunksNode, this.type.chunkDepth, indexes, nodes, hashCompsThis); + + this._rootNode = this.type.tree_setChunksNode( + this._rootNode, + newChunksNode, + this.dirtyLength ? this._length : null, + hashComps + ); + + if (!isOldRootHashed && hashComps !== null) { + // should never happen, handle just in case + // not possible to get HashComputations due to BranchNodeStruct + this._rootNode.root; + } + + this.viewsChanged.clear(); + this.dirtyLength = false; + } +} diff --git a/packages/types/src/phase0/viewDU/validator.ts b/packages/types/src/phase0/viewDU/validator.ts new file mode 100644 index 000000000000..02cead19bada --- /dev/null +++ b/packages/types/src/phase0/viewDU/validator.ts @@ -0,0 +1,284 @@ +import {byteArrayIntoHashObject} from "@chainsafe/as-sha256"; +import {Node, digestNLevelUnsafe} from "@chainsafe/persistent-merkle-tree"; +import {ByteViews, TreeViewDU, ContainerTypeGeneric, BranchNodeStruct} from "@chainsafe/ssz"; +import {ValidatorType} from "../validator.js"; +type Validator = { + pubkey: Uint8Array; + withdrawalCredentials: Uint8Array; + effectiveBalance: number; + slashed: boolean; + activationEligibilityEpoch: number; + activationEpoch: number; + exitEpoch: number; + withdrawableEpoch: number; +}; + +const numFields = 8; +const NUMBER_2_POW_32 = 2 ** 32; +/* + * Below constants are respective to their ssz type in `ValidatorType`. + */ +const UINT32_SIZE = 4; +const CHUNK_SIZE = 32; + +// validator has 8 nodes at level 3 +const singleLevel3Bytes = new Uint8Array(8 * 32); +const singleLevel3ByteView = {uint8Array: singleLevel3Bytes, dataView: new DataView(singleLevel3Bytes.buffer)}; +// validator has 2 nodes at level 4 (pubkey has 48 bytes = 2 * nodes) +const singleLevel4Bytes = new Uint8Array(2 * 32); + +/** + * A specific ViewDU for validator designed to be efficient to batch hash and efficient to create tree + * because it uses prepopulated nodes to do that. + */ +export class ValidatorTreeViewDU extends TreeViewDU> { + protected valueChanged: Validator | null = null; + protected _rootNode: BranchNodeStruct; + + constructor( + readonly type: ContainerTypeGeneric, + node: Node + ) { + super(); + this._rootNode = node as BranchNodeStruct; + } + + get node(): Node { + return this._rootNode; + } + + get cache(): void { + return; + } + + /** + * Commit the changes to the tree, need to hashTreeRoot() because this does not support batch hash + */ + commit(): void { + if (this.valueChanged !== null) { + this._rootNode = this.type.value_toTree(this.valueChanged) as BranchNodeStruct; + } + + if (this._rootNode.h0 === null) { + this.valueToMerkleBytes(singleLevel3ByteView, singleLevel4Bytes); + // level 4 hash + const pubkeyRoot = digestNLevelUnsafe(singleLevel4Bytes, 1); + if (pubkeyRoot.length !== 32) { + throw new Error(`Invalid pubkeyRoot length, expect 32, got ${pubkeyRoot.length}`); + } + singleLevel3ByteView.uint8Array.set(pubkeyRoot, 0); + // level 3 hash + const validatorRoot = digestNLevelUnsafe(singleLevel3ByteView.uint8Array, 3); + if (validatorRoot.length !== 32) { + throw new Error(`Invalid validatorRoot length, expect 32, got ${validatorRoot.length}`); + } + byteArrayIntoHashObject(validatorRoot, this._rootNode); + } + this.valueChanged = null; + } + + get pubkey(): Uint8Array { + return (this.valueChanged || this._rootNode.value).pubkey; + } + + set pubkey(value: Uint8Array) { + if (this.valueChanged === null) { + this.valueChanged = this.type.clone(this._rootNode.value); + } + + this.valueChanged.pubkey = value.slice(); + } + + get withdrawalCredentials(): Uint8Array { + return (this.valueChanged || this._rootNode.value).withdrawalCredentials; + } + + set withdrawalCredentials(value: Uint8Array) { + if (this.valueChanged === null) { + this.valueChanged = this.type.clone(this._rootNode.value); + } + + this.valueChanged.withdrawalCredentials = value.slice(); + } + + get effectiveBalance(): number { + return (this.valueChanged || this._rootNode.value).effectiveBalance; + } + + set effectiveBalance(value: number) { + if (this.valueChanged === null) { + this.valueChanged = this.type.clone(this._rootNode.value); + } + + this.valueChanged.effectiveBalance = value; + } + + get slashed(): boolean { + return (this.valueChanged || this._rootNode.value).slashed; + } + + set slashed(value: boolean) { + if (this.valueChanged === null) { + this.valueChanged = this.type.clone(this._rootNode.value); + } + + this.valueChanged.slashed = value; + } + + get activationEligibilityEpoch(): number { + return (this.valueChanged || this._rootNode.value).activationEligibilityEpoch; + } + + set activationEligibilityEpoch(value: number) { + if (this.valueChanged === null) { + this.valueChanged = this.type.clone(this._rootNode.value); + } + + this.valueChanged.activationEligibilityEpoch = value; + } + + get activationEpoch(): number { + return (this.valueChanged || this._rootNode.value).activationEpoch; + } + + set activationEpoch(value: number) { + if (this.valueChanged === null) { + this.valueChanged = this.type.clone(this._rootNode.value); + } + + this.valueChanged.activationEpoch = value; + } + + get exitEpoch(): number { + return (this.valueChanged || this._rootNode.value).exitEpoch; + } + + set exitEpoch(value: number) { + if (this.valueChanged === null) { + this.valueChanged = this.type.clone(this._rootNode.value); + } + + this.valueChanged.exitEpoch = value; + } + + get withdrawableEpoch(): number { + return (this.valueChanged || this._rootNode.value).withdrawableEpoch; + } + + set withdrawableEpoch(value: number) { + if (this.valueChanged === null) { + this.valueChanged = this.type.clone(this._rootNode.value); + } + + this.valueChanged.withdrawableEpoch = value; + } + + /** + * Write to level3 and level4 bytes to compute merkle root. Note that this is to compute + * merkle root and it's different from serialization (which is more compressed). + * pub0 + pub1 are at level4, they will be hashed to 1st chunked of level 3 + * then use 8 chunks of level 3 to compute the root hash. + * reserved withdr eff sla actElig act exit with + * level 3 |----------|----------|----------|----------|----------|----------|----------|----------| + * + * pub0 pub1 + * level4 |----------|----------| + * + */ + valueToMerkleBytes(level3: ByteViews, level4: Uint8Array): void { + if (level3.uint8Array.byteLength !== 8 * CHUNK_SIZE) { + throw Error(`Expected level3 to be 8 * CHUNK_SIZE bytes, got ${level3.uint8Array.byteLength}`); + } + if (level4.length !== 2 * CHUNK_SIZE) { + throw Error(`Expected level4 to be 2 * CHUNK_SIZE bytes, got ${level4.length}`); + } + // in case pushing a new validator to array, valueChanged could be null + const value = this.valueChanged ?? this._rootNode.value; + validatorToMerkleBytes(level3, level4, value); + } + + /** + * Batch hash flow: parent will compute hash and call this function + */ + commitToRoot(root: Uint8Array): void { + // this.valueChanged === null means this viewDU is new + if (this.valueChanged !== null) { + this._rootNode = this.type.value_toTree(this.valueChanged) as BranchNodeStruct; + } + byteArrayIntoHashObject(root, this._rootNode); + this.valueChanged = null; + } + + protected clearCache(): void { + this.valueChanged = null; + } + + get name(): string { + return this.type.typeName; + } +} + +/** + * Write to level3 and level4 bytes to compute merkle root. Note that this is to compute + * merkle root and it's different from serialization (which is more compressed). + * pub0 + pub1 are at level4, they will be hashed to 1st chunked of level 3 + * then use 8 chunks of level 3 to compute the root hash. + * reserved withdr eff sla actElig act exit with + * level 3 |----------|----------|----------|----------|----------|----------|----------|----------| + * + * pub0 pub1 + * level4 |----------|----------| + * + */ +export function validatorToMerkleBytes(level3: ByteViews, level4: Uint8Array, value: Validator): void { + const { + pubkey, + withdrawalCredentials, + effectiveBalance, + slashed, + activationEligibilityEpoch, + activationEpoch, + exitEpoch, + withdrawableEpoch, + } = value; + const {uint8Array: outputLevel3, dataView} = level3; + + // pubkey = 48 bytes which is 2 * CHUNK_SIZE + level4.set(pubkey, 0); + let offset = CHUNK_SIZE; + outputLevel3.set(withdrawalCredentials, offset); + offset += CHUNK_SIZE; + // effectiveBalance is UintNum64 + dataView.setUint32(offset, effectiveBalance & 0xffffffff, true); + dataView.setUint32(offset + 4, (effectiveBalance / NUMBER_2_POW_32) & 0xffffffff, true); + + offset += CHUNK_SIZE; + // output[offset] = validator.slashed ? 1 : 0; + dataView.setUint32(offset, slashed ? 1 : 0, true); + offset += CHUNK_SIZE; + writeEpochInf(dataView, offset, activationEligibilityEpoch); + offset += CHUNK_SIZE; + writeEpochInf(dataView, offset, activationEpoch); + offset += CHUNK_SIZE; + writeEpochInf(dataView, offset, exitEpoch); + offset += CHUNK_SIZE; + writeEpochInf(dataView, offset, withdrawableEpoch); +} + + +/** + * Write an epoch to DataView at offset. + */ +function writeEpochInf(dataView: DataView, offset: number, value: number): void { + if (value === Infinity) { + dataView.setUint32(offset, 0xffffffff, true); + offset += UINT32_SIZE; + dataView.setUint32(offset, 0xffffffff, true); + offset += UINT32_SIZE; + } else { + dataView.setUint32(offset, value & 0xffffffff, true); + offset += UINT32_SIZE; + dataView.setUint32(offset, (value / NUMBER_2_POW_32) & 0xffffffff, true); + offset += UINT32_SIZE; + } +} diff --git a/packages/utils/package.json b/packages/utils/package.json index e3ada3e2901b..3d83a96c0c35 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -39,7 +39,7 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@chainsafe/as-sha256": "^0.4.1", + "@chainsafe/as-sha256": "file:../../../ssz/packages/as-sha256", "any-signal": "3.0.1", "bigint-buffer": "^1.1.5", "case": "^1.6.3", diff --git a/packages/validator/package.json b/packages/validator/package.json index abf5f8797d89..836aa3ec18f8 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -46,7 +46,7 @@ ], "dependencies": { "@chainsafe/blst": "^2.0.3", - "@chainsafe/ssz": "^0.15.1", + "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@lodestar/api": "^1.20.2", "@lodestar/config": "^1.20.2", "@lodestar/db": "^1.20.2", diff --git a/yarn.lock b/yarn.lock index efca10e5aa8c..0004a52cf159 100644 --- a/yarn.lock +++ b/yarn.lock @@ -308,6 +308,9 @@ resolved "https://registry.yarnpkg.com/@chainsafe/as-chacha20poly1305/-/as-chacha20poly1305-0.1.0.tgz#7da6f8796f9b42dac6e830a086d964f1f9189e09" integrity sha512-BpNcL8/lji/GM3+vZ/bgRWqJ1q5kwvTFmGPk7pxm/QQZDbaMI98waOHjEymTjq2JmdD/INdNBFOVSyJofXg7ew== +"@chainsafe/as-sha256@0.4.2", "@chainsafe/as-sha256@file:../ssz/packages/as-sha256": + version "0.4.2" + "@chainsafe/as-sha256@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.4.1.tgz#cfc0737e25f8c206767bdb6703e7943e5d44513e" @@ -453,6 +456,30 @@ optionalDependencies: "@node-rs/crc32" "^1.6.0" +"@chainsafe/hashtree-darwin-arm64@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@chainsafe/hashtree-darwin-arm64/-/hashtree-darwin-arm64-1.0.1.tgz#e2c60090c56a1c8dc8bdff329856184ad32e4cd5" + integrity sha512-+KmEgQMpO7FDL3klAcpXbQ4DPZvfCe0qSaBBrtT4vLF8V1JGm3sp+j7oibtxtOsLKz7nJMiK1pZExi7vjXu8og== + +"@chainsafe/hashtree-linux-arm64-gnu@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@chainsafe/hashtree-linux-arm64-gnu/-/hashtree-linux-arm64-gnu-1.0.1.tgz#49d2604a6c9106219448af3eaf76f4da6e44daca" + integrity sha512-p1hnhGq2aFY+Zhdn1Q6L/6yLYNKjqXfn/Pc8jiM0e3+Lf/hB+yCdqYVu1pto26BrZjugCFZfupHaL4DjUTDttw== + +"@chainsafe/hashtree-linux-x64-gnu@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@chainsafe/hashtree-linux-x64-gnu/-/hashtree-linux-x64-gnu-1.0.1.tgz#31c5a2bb196b78f04f2bf4bfb5c1bf1f3331f071" + integrity sha512-uCIGuUWuWV0LiB4KLMy6JFa7Jp6NmPl3hKF5BYWu8TzUBe7vSXMZfqTzGxXPggFYN2/0KymfRdG9iDCOJfGRqg== + +"@chainsafe/hashtree@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@chainsafe/hashtree/-/hashtree-1.0.1.tgz#587666a261e1da6a37904095ce875fddc53c7c89" + integrity sha512-bleu9FjqBeR/l6W1u2Lz+HsS0b0LLJX2eUt3hOPBN7VqOhidx8wzkVh2S7YurS+iTQtfdK4K5QU9tcTGNrGwDg== + optionalDependencies: + "@chainsafe/hashtree-darwin-arm64" "1.0.1" + "@chainsafe/hashtree-linux-arm64-gnu" "1.0.1" + "@chainsafe/hashtree-linux-x64-gnu" "1.0.1" + "@chainsafe/is-ip@^2.0.1": version "2.0.1" resolved "https://registry.yarnpkg.com/@chainsafe/is-ip/-/is-ip-2.0.1.tgz#62cb285669d91f88fd9fa285048dde3882f0993b" @@ -530,6 +557,13 @@ dependencies: "@chainsafe/is-ip" "^2.0.1" +"@chainsafe/persistent-merkle-tree@0.7.2", "@chainsafe/persistent-merkle-tree@file:../ssz/packages/persistent-merkle-tree": + version "0.7.2" + dependencies: + "@chainsafe/as-sha256" "0.4.2" + "@chainsafe/hashtree" "1.0.1" + "@noble/hashes" "^1.3.0" + "@chainsafe/persistent-merkle-tree@^0.6.1": version "0.6.1" resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.6.1.tgz#37bde25cf6cbe1660ad84311aa73157dc86ec7f2" @@ -538,14 +572,6 @@ "@chainsafe/as-sha256" "^0.4.1" "@noble/hashes" "^1.3.0" -"@chainsafe/persistent-merkle-tree@^0.7.1": - version "0.7.1" - resolved "https://registry.yarnpkg.com/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.7.1.tgz#bfe6b3f4296ed3a578bb4fe69f9a7c232822a1dc" - integrity sha512-GUomb8DgkbHyKVBoLY9wMBe67oyAK9HKMjPImIocGOJuwqqxvDbVwh/ihdzyOrhEyhISqD/TxhCBDEXzLM52Vg== - dependencies: - "@chainsafe/as-sha256" "^0.4.1" - "@noble/hashes" "^1.3.0" - "@chainsafe/persistent-ts@^0.19.1": version "0.19.1" resolved "https://registry.npmjs.org/@chainsafe/persistent-ts/-/persistent-ts-0.19.1.tgz" @@ -564,13 +590,11 @@ "@chainsafe/as-sha256" "^0.4.1" "@chainsafe/persistent-merkle-tree" "^0.6.1" -"@chainsafe/ssz@^0.15.1": - version "0.15.1" - resolved "https://registry.yarnpkg.com/@chainsafe/ssz/-/ssz-0.15.1.tgz#008a711c3bcdc0d207cd4be15108870b0b1c60c0" - integrity sha512-f09UKTyYwWA1nr1BwrwsFpkXMspDDIZtwWXK1pM5mpPMnexmuPVstnN+P0M4YJ2aHcfqJXG7QOqnOwGj5Z7bUw== +"@chainsafe/ssz@file:../ssz/packages/ssz": + version "0.16.0" dependencies: - "@chainsafe/as-sha256" "^0.4.1" - "@chainsafe/persistent-merkle-tree" "^0.7.1" + "@chainsafe/as-sha256" "0.4.2" + "@chainsafe/persistent-merkle-tree" "0.7.2" "@chainsafe/threads@^1.11.1": version "1.11.1" From 4f16ac442fb72a37da36e56b82598b415a5af516 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 4 Jul 2024 15:54:16 +0700 Subject: [PATCH 02/22] fix: update validator ViewDUs from ssz --- .../types/src/phase0/viewDU/listValidator.ts | 40 ++++++++++-------- packages/types/src/phase0/viewDU/validator.ts | 42 +++++-------------- 2 files changed, 32 insertions(+), 50 deletions(-) diff --git a/packages/types/src/phase0/viewDU/listValidator.ts b/packages/types/src/phase0/viewDU/listValidator.ts index 36facd993631..f42f7bf538b4 100644 --- a/packages/types/src/phase0/viewDU/listValidator.ts +++ b/packages/types/src/phase0/viewDU/listValidator.ts @@ -2,7 +2,7 @@ import {ListCompositeType, ArrayCompositeTreeViewDUCache, ListCompositeTreeViewD import { HashComputationGroup, Node, - digestNLevelUnsafe, + digestNLevel, setNodesAtDepth, } from "@chainsafe/persistent-merkle-tree"; import {ValidatorNodeStructType} from "../validator.js"; @@ -12,11 +12,12 @@ import {ValidatorTreeViewDU} from "./validator.js"; /** * hashtree has a MAX_SIZE of 1024 bytes = 32 chunks * Given a level3 of validators have 8 chunks, we can hash 4 validators at a time -*/ + */ const PARALLEL_FACTOR = 4; /** * Allocate memory once for batch hash validators. */ +// each level 3 of validator has 8 chunks, each chunk has 32 bytes const batchLevel3Bytes = new Uint8Array(PARALLEL_FACTOR * 8 * 32); const level3ByteViewsArr: ByteViews[] = []; for (let i = 0; i < PARALLEL_FACTOR; i++) { @@ -24,24 +25,29 @@ for (let i = 0; i < PARALLEL_FACTOR; i++) { const dataView = new DataView(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteLength); level3ByteViewsArr.push({uint8Array, dataView}); } - // each level 4 of validator has 2 chunks for pubkey, each chunk has 32 bytes const batchLevel4Bytes = new Uint8Array(PARALLEL_FACTOR * 2 * 32); const level4BytesArr: Uint8Array[] = []; for (let i = 0; i < PARALLEL_FACTOR; i++) { level4BytesArr.push(batchLevel4Bytes.subarray(i * 2 * 32, (i + 1) * 2 * 32)); } +const pubkeyRoots: Uint8Array[] = []; +for (let i = 0; i < PARALLEL_FACTOR; i++) { + pubkeyRoots.push(batchLevel4Bytes.subarray(i * 32, (i + 1) * 32)); +} +const validatorRoots: Uint8Array[] = []; +for (let i = 0; i < PARALLEL_FACTOR; i++) { + validatorRoots.push(batchLevel3Bytes.subarray(i * 32, (i + 1) * 32)); +} export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU { - constructor( readonly type: ListCompositeType, protected _rootNode: Node, cache?: ArrayCompositeTreeViewDUCache ) { super(type, _rootNode, cache); - // each level 3 of validator has 8 chunks, each chunk has 32 bytes } commit(hashComps: HashComputationGroup | null = null): void { @@ -61,31 +67,29 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU= 0; j--) { const viewIndex = indicesChanged[i - j]; const indexInBatch = (i - j) % PARALLEL_FACTOR; - const validatorRoot = validatorRoots.subarray(indexInBatch * 32, (indexInBatch + 1) * 32); const viewChanged = this.viewsChanged.get(viewIndex) as ValidatorTreeViewDU; - viewChanged.commitToRoot(validatorRoot); + viewChanged.commitToRoot(validatorRoots[indexInBatch]); nodesChanged.push({index: viewIndex, node: viewChanged.node}); // Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal this.nodes[viewIndex] = viewChanged.node; diff --git a/packages/types/src/phase0/viewDU/validator.ts b/packages/types/src/phase0/viewDU/validator.ts index 02cead19bada..3445875215f7 100644 --- a/packages/types/src/phase0/viewDU/validator.ts +++ b/packages/types/src/phase0/viewDU/validator.ts @@ -1,5 +1,5 @@ import {byteArrayIntoHashObject} from "@chainsafe/as-sha256"; -import {Node, digestNLevelUnsafe} from "@chainsafe/persistent-merkle-tree"; +import {Node} from "@chainsafe/persistent-merkle-tree"; import {ByteViews, TreeViewDU, ContainerTypeGeneric, BranchNodeStruct} from "@chainsafe/ssz"; import {ValidatorType} from "../validator.js"; type Validator = { @@ -13,7 +13,6 @@ type Validator = { withdrawableEpoch: number; }; -const numFields = 8; const NUMBER_2_POW_32 = 2 ** 32; /* * Below constants are respective to their ssz type in `ValidatorType`. @@ -21,11 +20,7 @@ const NUMBER_2_POW_32 = 2 ** 32; const UINT32_SIZE = 4; const CHUNK_SIZE = 32; -// validator has 8 nodes at level 3 -const singleLevel3Bytes = new Uint8Array(8 * 32); -const singleLevel3ByteView = {uint8Array: singleLevel3Bytes, dataView: new DataView(singleLevel3Bytes.buffer)}; -// validator has 2 nodes at level 4 (pubkey has 48 bytes = 2 * nodes) -const singleLevel4Bytes = new Uint8Array(2 * 32); +const temporaryRoot = new Uint8Array(32); /** * A specific ViewDU for validator designed to be efficient to batch hash and efficient to create tree @@ -35,10 +30,7 @@ export class ValidatorTreeViewDU extends TreeViewDU; - constructor( - readonly type: ContainerTypeGeneric, - node: Node - ) { + constructor(readonly type: ContainerTypeGeneric, node: Node) { super(); this._rootNode = node as BranchNodeStruct; } @@ -51,28 +43,15 @@ export class ValidatorTreeViewDU extends TreeViewDU; } if (this._rootNode.h0 === null) { - this.valueToMerkleBytes(singleLevel3ByteView, singleLevel4Bytes); - // level 4 hash - const pubkeyRoot = digestNLevelUnsafe(singleLevel4Bytes, 1); - if (pubkeyRoot.length !== 32) { - throw new Error(`Invalid pubkeyRoot length, expect 32, got ${pubkeyRoot.length}`); - } - singleLevel3ByteView.uint8Array.set(pubkeyRoot, 0); - // level 3 hash - const validatorRoot = digestNLevelUnsafe(singleLevel3ByteView.uint8Array, 3); - if (validatorRoot.length !== 32) { - throw new Error(`Invalid validatorRoot length, expect 32, got ${validatorRoot.length}`); - } - byteArrayIntoHashObject(validatorRoot, this._rootNode); + const value = this.valueChanged ?? this._rootNode.value; + this.type.hashTreeRootInto(value, temporaryRoot, 0); + byteArrayIntoHashObject(temporaryRoot, 0, this._rootNode); } this.valueChanged = null; } @@ -185,7 +164,7 @@ export class ValidatorTreeViewDU extends TreeViewDU; } - byteArrayIntoHashObject(root, this._rootNode); + byteArrayIntoHashObject(root, 0, this._rootNode); this.valueChanged = null; } @@ -230,7 +209,7 @@ export class ValidatorTreeViewDU extends TreeViewDU Date: Sat, 6 Jul 2024 13:47:39 +0700 Subject: [PATCH 03/22] fix: only overwrite ListValidatorViewDU --- packages/types/src/phase0/validator.ts | 60 +++- .../types/src/phase0/viewDU/listValidator.ts | 55 ++-- packages/types/src/phase0/viewDU/validator.ts | 262 ------------------ 3 files changed, 87 insertions(+), 290 deletions(-) delete mode 100644 packages/types/src/phase0/viewDU/validator.ts diff --git a/packages/types/src/phase0/validator.ts b/packages/types/src/phase0/validator.ts index 5768ed9d36e3..2861f52d2deb 100644 --- a/packages/types/src/phase0/validator.ts +++ b/packages/types/src/phase0/validator.ts @@ -1,7 +1,5 @@ import {ByteViews, ContainerNodeStructType, ValueOfFields} from "@chainsafe/ssz"; -import {Node} from "@chainsafe/persistent-merkle-tree"; import * as primitiveSsz from "../primitive/sszTypes.js"; -import {ValidatorTreeViewDU} from "./viewDU/validator.js"; const {Boolean, Bytes32, UintNum64, BLSPubkey, EpochInf} = primitiveSsz; @@ -15,6 +13,7 @@ const UINT32_SIZE = 4; const PUBKEY_SIZE = 48; const WITHDRAWAL_CREDENTIALS_SIZE = 32; const SLASHED_SIZE = 1; +const CHUNK_SIZE = 32; export const ValidatorType = { pubkey: BLSPubkey, @@ -35,10 +34,6 @@ export class ValidatorNodeStructType extends ContainerNodeStructType +): void { + const { + pubkey, + withdrawalCredentials, + effectiveBalance, + slashed, + activationEligibilityEpoch, + activationEpoch, + exitEpoch, + withdrawableEpoch, + } = value; + const {uint8Array: outputLevel3, dataView} = level3; + + // pubkey = 48 bytes which is 2 * CHUNK_SIZE + level4.set(pubkey, 0); + let offset = CHUNK_SIZE; + outputLevel3.set(withdrawalCredentials, offset); + offset += CHUNK_SIZE; + // effectiveBalance is UintNum64 + dataView.setUint32(offset, effectiveBalance & 0xffffffff, true); + dataView.setUint32(offset + 4, (effectiveBalance / NUMBER_2_POW_32) & 0xffffffff, true); + + offset += CHUNK_SIZE; + dataView.setUint32(offset, slashed ? 1 : 0, true); + offset += CHUNK_SIZE; + writeEpochInf(dataView, offset, activationEligibilityEpoch); + offset += CHUNK_SIZE; + writeEpochInf(dataView, offset, activationEpoch); + offset += CHUNK_SIZE; + writeEpochInf(dataView, offset, exitEpoch); + offset += CHUNK_SIZE; + writeEpochInf(dataView, offset, withdrawableEpoch); +} + function writeEpochInf(dataView: DataView, offset: number, value: number): number { if (value === Infinity) { dataView.setUint32(offset, 0xffffffff, true); @@ -79,4 +126,3 @@ function writeEpochInf(dataView: DataView, offset: number, value: number): numbe } return offset; } -export const ValidatorNodeStruct = new ValidatorNodeStructType(); diff --git a/packages/types/src/phase0/viewDU/listValidator.ts b/packages/types/src/phase0/viewDU/listValidator.ts index f42f7bf538b4..30c6cbebfaf4 100644 --- a/packages/types/src/phase0/viewDU/listValidator.ts +++ b/packages/types/src/phase0/viewDU/listValidator.ts @@ -1,13 +1,13 @@ -import {ListCompositeType, ArrayCompositeTreeViewDUCache, ListCompositeTreeViewDU, ByteViews} from "@chainsafe/ssz"; import { - HashComputationGroup, - Node, - digestNLevel, - setNodesAtDepth, -} from "@chainsafe/persistent-merkle-tree"; -import {ValidatorNodeStructType} from "../validator.js"; -import {ValidatorTreeViewDU} from "./validator.js"; - + ListCompositeType, + ArrayCompositeTreeViewDUCache, + ListCompositeTreeViewDU, + ByteViews, + ContainerNodeStructTreeViewDU, +} from "@chainsafe/ssz"; +import {HashComputationGroup, Node, digestNLevel, setNodesAtDepth} from "@chainsafe/persistent-merkle-tree"; +import {byteArrayIntoHashObject} from "@chainsafe/as-sha256"; +import {ValidatorNodeStructType, ValidatorType, validatorToChunkBytes} from "../validator.js"; /** * hashtree has a MAX_SIZE of 1024 bytes = 32 chunks @@ -61,6 +61,10 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU + >; const indicesChanged = Array.from(this.viewsChanged.keys()).sort((a, b) => a - b); const endBatch = indicesChanged.length - (indicesChanged.length % PARALLEL_FACTOR); // nodesChanged is sorted by index @@ -73,8 +77,10 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU= 0; j--) { const viewIndex = indicesChanged[i - j]; const indexInBatch = (i - j) % PARALLEL_FACTOR; - const viewChanged = this.viewsChanged.get(viewIndex) as ValidatorTreeViewDU; - viewChanged.commitToRoot(validatorRoots[indexInBatch]); - nodesChanged.push({index: viewIndex, node: viewChanged.node}); - // Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal - this.nodes[viewIndex] = viewChanged.node; + const viewChanged = viewsChanged.get(viewIndex); + if (viewChanged) { + viewChanged.commitNoHash(); + const branchNodeStruct = viewChanged.node; + byteArrayIntoHashObject(validatorRoots[indexInBatch], 0, branchNodeStruct); + nodesChanged.push({index: viewIndex, node: viewChanged.node}); + // Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal + this.nodes[viewIndex] = viewChanged.node; + } } } } @@ -101,11 +111,14 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU> { - protected valueChanged: Validator | null = null; - protected _rootNode: BranchNodeStruct; - - constructor(readonly type: ContainerTypeGeneric, node: Node) { - super(); - this._rootNode = node as BranchNodeStruct; - } - - get node(): Node { - return this._rootNode; - } - - get cache(): void { - return; - } - - commit(): void { - if (this.valueChanged !== null) { - this._rootNode = this.type.value_toTree(this.valueChanged) as BranchNodeStruct; - } - - if (this._rootNode.h0 === null) { - const value = this.valueChanged ?? this._rootNode.value; - this.type.hashTreeRootInto(value, temporaryRoot, 0); - byteArrayIntoHashObject(temporaryRoot, 0, this._rootNode); - } - this.valueChanged = null; - } - - get pubkey(): Uint8Array { - return (this.valueChanged || this._rootNode.value).pubkey; - } - - set pubkey(value: Uint8Array) { - if (this.valueChanged === null) { - this.valueChanged = this.type.clone(this._rootNode.value); - } - - this.valueChanged.pubkey = value.slice(); - } - - get withdrawalCredentials(): Uint8Array { - return (this.valueChanged || this._rootNode.value).withdrawalCredentials; - } - - set withdrawalCredentials(value: Uint8Array) { - if (this.valueChanged === null) { - this.valueChanged = this.type.clone(this._rootNode.value); - } - - this.valueChanged.withdrawalCredentials = value.slice(); - } - - get effectiveBalance(): number { - return (this.valueChanged || this._rootNode.value).effectiveBalance; - } - - set effectiveBalance(value: number) { - if (this.valueChanged === null) { - this.valueChanged = this.type.clone(this._rootNode.value); - } - - this.valueChanged.effectiveBalance = value; - } - - get slashed(): boolean { - return (this.valueChanged || this._rootNode.value).slashed; - } - - set slashed(value: boolean) { - if (this.valueChanged === null) { - this.valueChanged = this.type.clone(this._rootNode.value); - } - - this.valueChanged.slashed = value; - } - - get activationEligibilityEpoch(): number { - return (this.valueChanged || this._rootNode.value).activationEligibilityEpoch; - } - - set activationEligibilityEpoch(value: number) { - if (this.valueChanged === null) { - this.valueChanged = this.type.clone(this._rootNode.value); - } - - this.valueChanged.activationEligibilityEpoch = value; - } - - get activationEpoch(): number { - return (this.valueChanged || this._rootNode.value).activationEpoch; - } - - set activationEpoch(value: number) { - if (this.valueChanged === null) { - this.valueChanged = this.type.clone(this._rootNode.value); - } - - this.valueChanged.activationEpoch = value; - } - - get exitEpoch(): number { - return (this.valueChanged || this._rootNode.value).exitEpoch; - } - - set exitEpoch(value: number) { - if (this.valueChanged === null) { - this.valueChanged = this.type.clone(this._rootNode.value); - } - - this.valueChanged.exitEpoch = value; - } - - get withdrawableEpoch(): number { - return (this.valueChanged || this._rootNode.value).withdrawableEpoch; - } - - set withdrawableEpoch(value: number) { - if (this.valueChanged === null) { - this.valueChanged = this.type.clone(this._rootNode.value); - } - - this.valueChanged.withdrawableEpoch = value; - } - - /** - * Write to level3 and level4 bytes to compute merkle root. Note that this is to compute - * merkle root and it's different from serialization (which is more compressed). - * pub0 + pub1 are at level4, they will be hashed to 1st chunked of level 3 - * then use 8 chunks of level 3 to compute the root hash. - * reserved withdr eff sla actElig act exit with - * level 3 |----------|----------|----------|----------|----------|----------|----------|----------| - * - * pub0 pub1 - * level4 |----------|----------| - * - */ - valueToChunkBytes(level3: ByteViews, level4: Uint8Array): void { - if (level3.uint8Array.byteLength !== 8 * CHUNK_SIZE) { - throw Error(`Expected level3 to be 8 * CHUNK_SIZE bytes, got ${level3.uint8Array.byteLength}`); - } - if (level4.length !== 2 * CHUNK_SIZE) { - throw Error(`Expected level4 to be 2 * CHUNK_SIZE bytes, got ${level4.length}`); - } - // in case pushing a new validator to array, valueChanged could be null - const value = this.valueChanged ?? this._rootNode.value; - validatorToChunkBytes(level3, level4, value); - } - - /** - * Batch hash flow: parent will compute hash and call this function - */ - commitToRoot(root: Uint8Array): void { - // this.valueChanged === null means this viewDU is new - if (this.valueChanged !== null) { - this._rootNode = this.type.value_toTree(this.valueChanged) as BranchNodeStruct; - } - byteArrayIntoHashObject(root, 0, this._rootNode); - this.valueChanged = null; - } - - protected clearCache(): void { - this.valueChanged = null; - } - - get name(): string { - return this.type.typeName; - } -} - -/** - * Write to level3 and level4 bytes to compute merkle root. Note that this is to compute - * merkle root and it's different from serialization (which is more compressed). - * pub0 + pub1 are at level4, they will be hashed to 1st chunked of level 3 - * then use 8 chunks of level 3 to compute the root hash. - * reserved withdr eff sla actElig act exit with - * level 3 |----------|----------|----------|----------|----------|----------|----------|----------| - * - * pub0 pub1 - * level4 |----------|----------| - * - */ -export function validatorToChunkBytes(level3: ByteViews, level4: Uint8Array, value: Validator): void { - const { - pubkey, - withdrawalCredentials, - effectiveBalance, - slashed, - activationEligibilityEpoch, - activationEpoch, - exitEpoch, - withdrawableEpoch, - } = value; - const {uint8Array: outputLevel3, dataView} = level3; - - // pubkey = 48 bytes which is 2 * CHUNK_SIZE - level4.set(pubkey, 0); - let offset = CHUNK_SIZE; - outputLevel3.set(withdrawalCredentials, offset); - offset += CHUNK_SIZE; - // effectiveBalance is UintNum64 - dataView.setUint32(offset, effectiveBalance & 0xffffffff, true); - dataView.setUint32(offset + 4, (effectiveBalance / NUMBER_2_POW_32) & 0xffffffff, true); - - offset += CHUNK_SIZE; - // output[offset] = validator.slashed ? 1 : 0; - dataView.setUint32(offset, slashed ? 1 : 0, true); - offset += CHUNK_SIZE; - writeEpochInf(dataView, offset, activationEligibilityEpoch); - offset += CHUNK_SIZE; - writeEpochInf(dataView, offset, activationEpoch); - offset += CHUNK_SIZE; - writeEpochInf(dataView, offset, exitEpoch); - offset += CHUNK_SIZE; - writeEpochInf(dataView, offset, withdrawableEpoch); -} - -/** - * Write an epoch to DataView at offset. - */ -function writeEpochInf(dataView: DataView, offset: number, value: number): void { - if (value === Infinity) { - dataView.setUint32(offset, 0xffffffff, true); - offset += UINT32_SIZE; - dataView.setUint32(offset, 0xffffffff, true); - offset += UINT32_SIZE; - } else { - dataView.setUint32(offset, value & 0xffffffff, true); - offset += UINT32_SIZE; - dataView.setUint32(offset, (value / NUMBER_2_POW_32) & 0xffffffff, true); - offset += UINT32_SIZE; - } -} From 57be07b22e4d80ffa80e29f65837181f1ae7f2e1 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 15 Jul 2024 14:01:26 +0700 Subject: [PATCH 04/22] feat: reuse arrays for getAll() api consumers --- .../src/block/processEth1Data.ts | 8 +++++++- .../src/cache/epochTransitionCache.ts | 18 ++++++++++++++---- .../src/epoch/getRewardsAndPenalties.ts | 15 ++++++++++++--- .../src/epoch/processInactivityUpdates.ts | 8 +++++++- .../src/epoch/processRewardsAndPenalties.ts | 7 ++++++- packages/state-transition/src/util/balance.ts | 10 ++++++++-- .../test/unit/cachedBeaconState.test.ts | 1 + 7 files changed, 55 insertions(+), 12 deletions(-) diff --git a/packages/state-transition/src/block/processEth1Data.ts b/packages/state-transition/src/block/processEth1Data.ts index 3d1927744328..2b6d8b62d79c 100644 --- a/packages/state-transition/src/block/processEth1Data.ts +++ b/packages/state-transition/src/block/processEth1Data.ts @@ -23,6 +23,10 @@ export function processEth1Data(state: CachedBeaconStateAllForks, eth1Data: phas state.eth1DataVotes.push(eth1DataView); } +/** + * This data is reused and never gc. + */ +const eth1DataVotes = new Array>(); /** * Returns true if adding the given `eth1Data` to `state.eth1DataVotes` would * result in a change to `state.eth1Data`. @@ -48,7 +52,9 @@ export function becomesNewEth1Data( // Then isEqualEth1DataView compares cached roots (HashObject as of Jan 2022) which is much cheaper // than doing structural equality, which requires tree -> value conversions let sameVotesCount = 0; - const eth1DataVotes = state.eth1DataVotes.getAllReadonly(); + // const eth1DataVotes = state.eth1DataVotes.getAllReadonly(); + eth1DataVotes.length = state.eth1DataVotes.length; + state.eth1DataVotes.getAllReadonly(eth1DataVotes); for (let i = 0; i < eth1DataVotes.length; i++) { if (isEqualEth1DataView(eth1DataVotes[i], newEth1Data)) { sameVotesCount++; diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index e6f84de6c62e..b9d0947a3bc0 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -1,4 +1,4 @@ -import {Epoch, ValidatorIndex} from "@lodestar/types"; +import {Epoch, ValidatorIndex, phase0} from "@lodestar/types"; import {intDiv} from "@lodestar/utils"; import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MAX_EFFECTIVE_BALANCE} from "@lodestar/params"; @@ -198,6 +198,12 @@ const inclusionDelays = new Array(); const flags = new Array(); /** WARNING: reused, never gc'd */ const nextEpochShufflingActiveValidatorIndices = new Array(); +/** + * This data is reused and never gc. + */ +const validators = new Array(); +const previousEpochParticipation = new Array(); +const currentEpochParticipation = new Array(); export function beforeProcessEpoch( state: CachedBeaconStateAllForks, @@ -223,7 +229,9 @@ export function beforeProcessEpoch( // To optimize memory each validator node in `state.validators` is represented with a special node type // `BranchNodeStruct` that represents the data as struct internally. This utility grabs the struct data directly // from the nodes without any extra transformation. The returned `validators` array contains native JS objects. - const validators = state.validators.getAllReadonlyValues(); + validators.length = state.validators.length; + state.validators.getAllReadonlyValues(validators); + const validatorCount = validators.length; nextEpochShufflingActiveValidatorIndices.length = validatorCount; @@ -391,8 +399,10 @@ export function beforeProcessEpoch( FLAG_CURR_HEAD_ATTESTER ); } else { - const previousEpochParticipation = (state as CachedBeaconStateAltair).previousEpochParticipation.getAll(); - const currentEpochParticipation = (state as CachedBeaconStateAltair).currentEpochParticipation.getAll(); + previousEpochParticipation.length = (state as CachedBeaconStateAltair).previousEpochParticipation.length; + (state as CachedBeaconStateAltair).previousEpochParticipation.getAll(previousEpochParticipation); + currentEpochParticipation.length = (state as CachedBeaconStateAltair).currentEpochParticipation.length; + (state as CachedBeaconStateAltair).currentEpochParticipation.getAll(currentEpochParticipation); for (let i = 0; i < validatorCount; i++) { flags[i] |= // checking active status first is required to pass random spec tests in altair diff --git a/packages/state-transition/src/epoch/getRewardsAndPenalties.ts b/packages/state-transition/src/epoch/getRewardsAndPenalties.ts index bf766fe4666a..f03b55804f6c 100644 --- a/packages/state-transition/src/epoch/getRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/getRewardsAndPenalties.ts @@ -17,7 +17,7 @@ import { FLAG_PREV_TARGET_ATTESTER_UNSLASHED, hasMarkers, } from "../util/attesterStatus.js"; -import {isInInactivityLeak, newZeroedArray} from "../util/index.js"; +import {isInInactivityLeak} from "../util/index.js"; type RewardPenaltyItem = { baseReward: number; @@ -28,6 +28,11 @@ type RewardPenaltyItem = { timelyHeadReward: number; }; +/** + * This data is reused and never gc. + */ +const rewards = new Array(); +const penalties = new Array(); /** * An aggregate of getFlagIndexDeltas and getInactivityPenaltyDeltas that loop through process.flags 1 time instead of 4. * @@ -48,8 +53,12 @@ export function getRewardsAndPenaltiesAltair( // TODO: Is there a cheaper way to measure length that going to `state.validators`? const validatorCount = state.validators.length; const activeIncrements = cache.totalActiveStakeByIncrement; - const rewards = newZeroedArray(validatorCount); - const penalties = newZeroedArray(validatorCount); + // const rewards = newZeroedArray(validatorCount); + // const penalties = newZeroedArray(validatorCount); + rewards.length = validatorCount; + rewards.fill(0); + penalties.length = validatorCount; + penalties.fill(0); const isInInactivityLeakBn = isInInactivityLeak(state); // effectiveBalance is multiple of EFFECTIVE_BALANCE_INCREMENT and less than MAX_EFFECTIVE_BALANCE diff --git a/packages/state-transition/src/epoch/processInactivityUpdates.ts b/packages/state-transition/src/epoch/processInactivityUpdates.ts index 4a9b129ee793..4d1e28d92bf4 100644 --- a/packages/state-transition/src/epoch/processInactivityUpdates.ts +++ b/packages/state-transition/src/epoch/processInactivityUpdates.ts @@ -3,6 +3,11 @@ import {CachedBeaconStateAltair, EpochTransitionCache} from "../types.js"; import * as attesterStatusUtil from "../util/attesterStatus.js"; import {isInInactivityLeak} from "../util/index.js"; +/** + * This data is reused and never gc. + */ +const inactivityScoresArr = new Array(); + /** * Mutates `inactivityScores` from pre-calculated validator flags. * @@ -30,7 +35,8 @@ export function processInactivityUpdates(state: CachedBeaconStateAltair, cache: // this avoids importing FLAG_ELIGIBLE_ATTESTER inside the for loop, check the compiled code const {FLAG_PREV_TARGET_ATTESTER_UNSLASHED, FLAG_ELIGIBLE_ATTESTER, hasMarkers} = attesterStatusUtil; - const inactivityScoresArr = inactivityScores.getAll(); + inactivityScoresArr.length = state.validators.length; + inactivityScores.getAll(inactivityScoresArr); for (let i = 0; i < flags.length; i++) { const flag = flags[i]; diff --git a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts index 61680b81002a..e342c1f71e78 100644 --- a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts @@ -9,6 +9,10 @@ import { import {getAttestationDeltas} from "./getAttestationDeltas.js"; import {getRewardsAndPenaltiesAltair} from "./getRewardsAndPenalties.js"; +/** + * This data is reused and never gc. + */ +const balances = new Array(); /** * Iterate over all validator and compute rewards and penalties to apply to balances. * @@ -25,7 +29,8 @@ export function processRewardsAndPenalties( } const [rewards, penalties] = getRewardsAndPenalties(state, cache); - const balances = state.balances.getAll(); + balances.length = state.balances.length; + state.balances.getAll(balances); for (let i = 0, len = rewards.length; i < len; i++) { balances[i] += rewards[i] - penalties[i] - (slashingPenalties[i] ?? 0); diff --git a/packages/state-transition/src/util/balance.ts b/packages/state-transition/src/util/balance.ts index e305c745ab72..edff9acef134 100644 --- a/packages/state-transition/src/util/balance.ts +++ b/packages/state-transition/src/util/balance.ts @@ -1,5 +1,5 @@ import {EFFECTIVE_BALANCE_INCREMENT} from "@lodestar/params"; -import {Gwei, ValidatorIndex} from "@lodestar/types"; +import {Gwei, ValidatorIndex, phase0} from "@lodestar/types"; import {bigIntMax} from "@lodestar/utils"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; import {BeaconStateAllForks} from ".."; @@ -43,6 +43,11 @@ export function decreaseBalance(state: BeaconStateAllForks, index: ValidatorInde state.balances.set(index, Math.max(0, newBalance)); } +/** + * This data is reused and never gc. + */ +const validators = new Array(); + /** * This method is used to get justified balances from a justified state. * This is consumed by forkchoice which based on delta so we return "by increment" (in ether) value, @@ -63,7 +68,8 @@ export function getEffectiveBalanceIncrementsZeroInactive( validatorCount ); - const validators = justifiedState.validators.getAllReadonly(); + validators.length = validatorCount; + justifiedState.validators.getAllReadonlyValues(validators); let j = 0; for (let i = 0; i < validatorCount; i++) { if (i === activeIndices[j]) { diff --git a/packages/state-transition/test/unit/cachedBeaconState.test.ts b/packages/state-transition/test/unit/cachedBeaconState.test.ts index 2891cd3e6216..f1a204ee2de4 100644 --- a/packages/state-transition/test/unit/cachedBeaconState.test.ts +++ b/packages/state-transition/test/unit/cachedBeaconState.test.ts @@ -84,6 +84,7 @@ describe("CachedBeaconState", () => { state.validators.get(i).effectiveBalance += 1; } } + state.commit(); if (validatorCountDelta < 0) { state.validators = state.validators.sliceTo(state.validators.length - 1 + validatorCountDelta); From 38b4905282f8c722c3dc92a2da79c9bd0d892db4 Mon Sep 17 00:00:00 2001 From: twoeths Date: Wed, 24 Jul 2024 13:50:29 +0700 Subject: [PATCH 05/22] feat: minimal alloc viewdu batch hash (#6977) * fix: sync listValidator.ts from ssz * fix: reuse HashComputationGroup in some flows * feat: use same HCGroup for epoch transition * fix: only batch hash balances of state in epoch transition --- .../blocks/verifyBlocksStateTransitionOnly.ts | 8 ++++++- .../beacon-node/src/chain/prepareNextSlot.ts | 17 +++++++++++++- .../epoch/processParticipationFlagUpdates.ts | 1 - .../types/src/phase0/viewDU/listValidator.ts | 22 ++++++++----------- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts index 49cd46220008..fda245bc5000 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts @@ -12,6 +12,12 @@ import {BlockProcessOpts} from "../options.js"; import {byteArrayEquals} from "../../util/bytes.js"; import {nextEventLoop} from "../../util/eventLoop.js"; import {BlockInput, ImportBlockOpts} from "./types.js"; +import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; + +/** + * Data in a BeaconBlock is bounded so we can use a single HashComputationGroup for all blocks + */ +const blockHCGroup = new HashComputationGroup(); /** * Verifies 1 or more blocks are fully valid running the full state transition; from a linear sequence of blocks. @@ -63,7 +69,7 @@ export async function verifyBlocksStateTransitionOnly( const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ source: StateHashTreeRootSource.blockTransition, }); - const stateRoot = postState.hashTreeRoot(); + const stateRoot = postState.hashTreeRoot(blockHCGroup); hashTreeRootTimer?.(); // Check state root matches diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index 48724ab25b0b..da77665377a5 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -18,6 +18,7 @@ import {isQueueErrorAborted} from "../util/queue/index.js"; import {prepareExecutionPayload, getPayloadAttributesForSSE} from "./produceBlock/produceBlockBody.js"; import {IBeaconChain} from "./interface.js"; import {RegenCaller} from "./regen/index.js"; +import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; /* With 12s slot times, this scheduler will run 4s before the start of each slot (`12 / 3 = 4`). */ export const SCHEDULER_LOOKAHEAD_FACTOR = 3; @@ -25,6 +26,11 @@ export const SCHEDULER_LOOKAHEAD_FACTOR = 3; /* We don't want to do more epoch transition than this */ const PREPARE_EPOCH_LIMIT = 1; +/** + * The same HashComputationGroup to be used for all epoch transition. + */ +const balancesHCGroup = new HashComputationGroup(); + /** * At Bellatrix, if we are responsible for proposing in next slot, we want to prepare payload * 4s (1/3 slot) before the start of next slot @@ -229,7 +235,16 @@ export class PrepareNextSlotScheduler { const hashTreeRootTimer = this.metrics?.stateHashTreeRootTime.startTimer({ source: isEpochTransition ? StateHashTreeRootSource.prepareNextEpoch : StateHashTreeRootSource.prepareNextSlot, }); - state.hashTreeRoot(); + if (isEpochTransition) { + // balances are completely changed per epoch and it's not much different so we can reuse the HashComputationGroup + state.balances.hashTreeRoot(balancesHCGroup); + // it's more performant to use normal hashTreeRoot() for the rest of the state + // this saves ~10ms per ~100ms as monitored on mainnet as of Jul 2024 + state.node.rootHashObject; + } else { + // normal slot, not worth to batch hash + state.node.rootHashObject; + } hashTreeRootTimer?.(); } } diff --git a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts index 3879d8ef8446..54d609cd1e16 100644 --- a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts +++ b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts @@ -21,7 +21,6 @@ export function processParticipationFlagUpdates(state: CachedBeaconStateAltair): state.currentEpochParticipation.node, zeroNode(ssz.altair.EpochParticipation.chunkDepth), state.currentEpochParticipation.length, - null ); state.currentEpochParticipation = ssz.altair.EpochParticipation.getViewDU(currentEpochParticipationNode); diff --git a/packages/types/src/phase0/viewDU/listValidator.ts b/packages/types/src/phase0/viewDU/listValidator.ts index 30c6cbebfaf4..aa518c4a0208 100644 --- a/packages/types/src/phase0/viewDU/listValidator.ts +++ b/packages/types/src/phase0/viewDU/listValidator.ts @@ -5,7 +5,7 @@ import { ByteViews, ContainerNodeStructTreeViewDU, } from "@chainsafe/ssz"; -import {HashComputationGroup, Node, digestNLevel, setNodesAtDepth} from "@chainsafe/persistent-merkle-tree"; +import {HashComputationGroup, HashComputationLevel, Node, digestNLevel, setNodesAtDepth} from "@chainsafe/persistent-merkle-tree"; import {byteArrayIntoHashObject} from "@chainsafe/as-sha256"; import {ValidatorNodeStructType, ValidatorType, validatorToChunkBytes} from "../validator.js"; @@ -50,10 +50,10 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU entry.index); const nodes = nodesChanged.map((entry) => entry.node); const chunksNode = this.type.tree_getChunksNode(this._rootNode); - const hashCompsThis = - hashComps != null && isOldRootHashed - ? { - byLevel: hashComps.byLevel, - offset: hashComps.offset + this.type.tree_chunksNodeOffset(), - } - : null; - const newChunksNode = setNodesAtDepth(chunksNode, this.type.chunkDepth, indexes, nodes, hashCompsThis); + const offsetThis = hcOffset + this.type.tree_chunksNodeOffset(); + const byLevelThis = hcByLevel != null && isOldRootHashed ? hcByLevel : null; + const newChunksNode = setNodesAtDepth(chunksNode, this.type.chunkDepth, indexes, nodes, offsetThis, byLevelThis); this._rootNode = this.type.tree_setChunksNode( this._rootNode, newChunksNode, this.dirtyLength ? this._length : null, - hashComps + hcOffset, + hcByLevel ); - if (!isOldRootHashed && hashComps !== null) { + if (!isOldRootHashed && hcByLevel !== null) { // should never happen, handle just in case // not possible to get HashComputations due to BranchNodeStruct this._rootNode.root; From e4e56d3b92d185166e028cbb47ef7094ba78f6bf Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 24 Jul 2024 15:56:57 +0700 Subject: [PATCH 06/22] feat: apply ReusableListIterator --- .../src/block/processEth1Data.ts | 14 ++++++------ .../src/cache/epochTransitionCache.ts | 22 +++++++++++-------- packages/state-transition/src/util/balance.ts | 14 +++++++----- 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/packages/state-transition/src/block/processEth1Data.ts b/packages/state-transition/src/block/processEth1Data.ts index 2b6d8b62d79c..fe40f026028a 100644 --- a/packages/state-transition/src/block/processEth1Data.ts +++ b/packages/state-transition/src/block/processEth1Data.ts @@ -1,5 +1,5 @@ import {Node} from "@chainsafe/persistent-merkle-tree"; -import {CompositeViewDU} from "@chainsafe/ssz"; +import {CompositeViewDU, ReusableListIterator} from "@chainsafe/ssz"; import {EPOCHS_PER_ETH1_VOTING_PERIOD, SLOTS_PER_EPOCH} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js"; @@ -26,7 +26,7 @@ export function processEth1Data(state: CachedBeaconStateAllForks, eth1Data: phas /** * This data is reused and never gc. */ -const eth1DataVotes = new Array>(); +const eth1DataVotes = new ReusableListIterator>(); /** * Returns true if adding the given `eth1Data` to `state.eth1DataVotes` would * result in a change to `state.eth1Data`. @@ -52,11 +52,11 @@ export function becomesNewEth1Data( // Then isEqualEth1DataView compares cached roots (HashObject as of Jan 2022) which is much cheaper // than doing structural equality, which requires tree -> value conversions let sameVotesCount = 0; - // const eth1DataVotes = state.eth1DataVotes.getAllReadonly(); - eth1DataVotes.length = state.eth1DataVotes.length; - state.eth1DataVotes.getAllReadonly(eth1DataVotes); - for (let i = 0; i < eth1DataVotes.length; i++) { - if (isEqualEth1DataView(eth1DataVotes[i], newEth1Data)) { + eth1DataVotes.reset(); + state.eth1DataVotes.getAllReadonlyIter(eth1DataVotes); + eth1DataVotes.clean(); + for (const eth1DataVote of eth1DataVotes) { + if (isEqualEth1DataView(eth1DataVote, newEth1Data)) { sameVotesCount++; } } diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index b9d0947a3bc0..21dbfc1588a2 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -16,6 +16,7 @@ import { import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../index.js"; import {computeBaseRewardPerIncrement} from "../util/altair.js"; import {processPendingAttestations} from "../epoch/processPendingAttestations.js"; +import {ReusableListIterator} from "@chainsafe/ssz"; export type EpochTransitionCacheOpts = { /** @@ -201,7 +202,7 @@ const nextEpochShufflingActiveValidatorIndices = new Array(); /** * This data is reused and never gc. */ -const validators = new Array(); +const validators = new ReusableListIterator(); const previousEpochParticipation = new Array(); const currentEpochParticipation = new Array(); @@ -221,7 +222,7 @@ export function beforeProcessEpoch( const indicesToSlash: ValidatorIndex[] = []; const indicesEligibleForActivationQueue: ValidatorIndex[] = []; - const indicesEligibleForActivation: ValidatorIndex[] = []; + const indicesEligibleForActivation: {validatorIndex: ValidatorIndex; activationEligibilityEpoch: Epoch}[] = []; const indicesToEject: ValidatorIndex[] = []; let totalActiveStakeByIncrement = 0; @@ -229,8 +230,9 @@ export function beforeProcessEpoch( // To optimize memory each validator node in `state.validators` is represented with a special node type // `BranchNodeStruct` that represents the data as struct internally. This utility grabs the struct data directly // from the nodes without any extra transformation. The returned `validators` array contains native JS objects. - validators.length = state.validators.length; - state.validators.getAllReadonlyValues(validators); + validators.reset(); + state.validators.getAllReadonlyValuesIter(validators); + validators.clean(); const validatorCount = validators.length; @@ -263,8 +265,9 @@ export function beforeProcessEpoch( const effectiveBalancesByIncrements = epochCtx.effectiveBalanceIncrements; - for (let i = 0; i < validatorCount; i++) { - const validator = validators[i]; + // for (let i = 0; i < validatorCount; i++) { + let i = 0; + for (const validator of validators) { let flag = 0; if (validator.slashed) { @@ -329,7 +332,7 @@ export function beforeProcessEpoch( // // Use `else` since indicesEligibleForActivationQueue + indicesEligibleForActivation are mutually exclusive else if (validator.activationEpoch === FAR_FUTURE_EPOCH && validator.activationEligibilityEpoch <= currentEpoch) { - indicesEligibleForActivation.push(i); + indicesEligibleForActivation.push({validatorIndex: i, activationEligibilityEpoch: validator.activationEligibilityEpoch}); } // To optimize process_registry_updates(): @@ -354,6 +357,7 @@ export function beforeProcessEpoch( if (isActiveNext2) { nextEpochShufflingActiveValidatorIndices[nextEpochShufflingActiveIndicesLength++] = i; } + i++; } if (totalActiveStakeByIncrement < 1) { @@ -368,7 +372,7 @@ export function beforeProcessEpoch( // To optimize process_registry_updates(): // order by sequence of activationEligibilityEpoch setting and then index indicesEligibleForActivation.sort( - (a, b) => validators[a].activationEligibilityEpoch - validators[b].activationEligibilityEpoch || a - b + (a, b) => a.activationEligibilityEpoch - b.activationEligibilityEpoch || a.validatorIndex - b.validatorIndex ); if (forkSeq === ForkSeq.phase0) { @@ -479,7 +483,7 @@ export function beforeProcessEpoch( currEpochUnslashedTargetStakeByIncrement: currTargetUnslStake, indicesToSlash, indicesEligibleForActivationQueue, - indicesEligibleForActivation, + indicesEligibleForActivation: indicesEligibleForActivation.map(({validatorIndex}) => validatorIndex), indicesToEject, nextEpochShufflingActiveValidatorIndices, nextEpochShufflingActiveIndicesLength, diff --git a/packages/state-transition/src/util/balance.ts b/packages/state-transition/src/util/balance.ts index edff9acef134..e14a18ab6606 100644 --- a/packages/state-transition/src/util/balance.ts +++ b/packages/state-transition/src/util/balance.ts @@ -4,6 +4,7 @@ import {bigIntMax} from "@lodestar/utils"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; import {BeaconStateAllForks} from ".."; import {CachedBeaconStateAllForks} from "../types.js"; +import {ReusableListIterator} from "@chainsafe/ssz"; /** * Return the combined effective balance of the [[indices]]. @@ -46,7 +47,7 @@ export function decreaseBalance(state: BeaconStateAllForks, index: ValidatorInde /** * This data is reused and never gc. */ -const validators = new Array(); +const validators = new ReusableListIterator(); /** * This method is used to get justified balances from a justified state. @@ -68,14 +69,16 @@ export function getEffectiveBalanceIncrementsZeroInactive( validatorCount ); - validators.length = validatorCount; - justifiedState.validators.getAllReadonlyValues(validators); + validators.reset(); + justifiedState.validators.getAllReadonlyValuesIter(validators); + validators.clean(); + let i = 0; let j = 0; - for (let i = 0; i < validatorCount; i++) { + for (const validator of validators) { if (i === activeIndices[j]) { // active validator j++; - if (validators[i].slashed) { + if (validator.slashed) { // slashed validator effectiveBalanceIncrementsZeroInactive[i] = 0; } @@ -83,6 +86,7 @@ export function getEffectiveBalanceIncrementsZeroInactive( // inactive validator effectiveBalanceIncrementsZeroInactive[i] = 0; } + i++; } return effectiveBalanceIncrementsZeroInactive; From 784a9bac92c75122581dbfb342b3a59f8b316fbf Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 2 Aug 2024 14:12:52 +0700 Subject: [PATCH 07/22] fix: skip light_client spec tests --- .../beacon-node/test/spec/utils/specTestIterator.ts | 2 +- packages/light-client/package.json | 2 ++ yarn.lock | 13 ++++++++++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 6aa683cb6530..059b0b76de79 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -65,7 +65,7 @@ export const defaultSkipOpts: SkipOpts = { "capella/light_client/single_merkle_proof/BeaconBlockBody", "deneb/light_client/single_merkle_proof/BeaconBlockBody", ], - skippedRunners: ["merkle_proof"], + skippedRunners: ["merkle_proof", "light_client"], }; /** diff --git a/packages/light-client/package.json b/packages/light-client/package.json index bf49935105e5..b4c747fa0d66 100644 --- a/packages/light-client/package.json +++ b/packages/light-client/package.json @@ -74,6 +74,8 @@ }, "dependencies": { "@chainsafe/bls": "7.1.3", + "@chainsafe/blst": "^0.2.0", + "@chainsafe/persistent-merkle-tree": "file:../../../ssz/packages/persistent-merkle-tree", "@chainsafe/ssz": "file:../../../ssz/packages/ssz", "@lodestar/api": "^1.20.2", diff --git a/yarn.lock b/yarn.lock index 0004a52cf159..0bce79326569 100644 --- a/yarn.lock +++ b/yarn.lock @@ -591,7 +591,7 @@ "@chainsafe/persistent-merkle-tree" "^0.6.1" "@chainsafe/ssz@file:../ssz/packages/ssz": - version "0.16.0" + version "0.17.0" dependencies: "@chainsafe/as-sha256" "0.4.2" "@chainsafe/persistent-merkle-tree" "0.7.2" @@ -3864,7 +3864,14 @@ agent-base@^7.0.2, agent-base@^7.1.0: dependencies: debug "^4.3.4" -agentkeepalive@^4.1.3, agentkeepalive@^4.2.1: +agentkeepalive@^4.1.3: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + +agentkeepalive@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.2.1.tgz#a7975cbb9f83b367f06c90cc51ff28fe7d499717" integrity sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA== @@ -4457,7 +4464,7 @@ bl@^5.0.0: bls-eth-wasm@^0.4.8: version "0.4.8" - resolved "https://registry.npmjs.org/bls-eth-wasm/-/bls-eth-wasm-0.4.8.tgz" + resolved "https://registry.yarnpkg.com/bls-eth-wasm/-/bls-eth-wasm-0.4.8.tgz#ad1818fbd1bfb64d8f3e6cd104bd28b96ebaa5f1" integrity sha512-ye7+G6KFLb3i9xSrLASAoYqOUK5WLB6XA5DD8Sh0UQpZ3T999ylsYbFdoOJpmvTDuBuMi23Vy8Jm0pn/GF01CA== bluebird@~3.4.1: From 4768db6d4d96f705ddfa8d3c4d13c10e35c91ee9 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 9 Aug 2024 16:03:54 +0700 Subject: [PATCH 08/22] feat: implement ViewDU.batchHashTreeRoot() --- .../blocks/verifyBlocksStateTransitionOnly.ts | 2 +- packages/beacon-node/src/chain/prepareNextSlot.ts | 4 ++-- packages/types/src/phase0/viewDU/listValidator.ts | 3 ++- yarn.lock | 14 +++++++------- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts index fda245bc5000..bbb17bfa624a 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts @@ -69,7 +69,7 @@ export async function verifyBlocksStateTransitionOnly( const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ source: StateHashTreeRootSource.blockTransition, }); - const stateRoot = postState.hashTreeRoot(blockHCGroup); + const stateRoot = postState.batchHashTreeRoot(blockHCGroup); hashTreeRootTimer?.(); // Check state root matches diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index da77665377a5..48d1d4af81f7 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -237,8 +237,8 @@ export class PrepareNextSlotScheduler { }); if (isEpochTransition) { // balances are completely changed per epoch and it's not much different so we can reuse the HashComputationGroup - state.balances.hashTreeRoot(balancesHCGroup); - // it's more performant to use normal hashTreeRoot() for the rest of the state + state.balances.batchHashTreeRoot(balancesHCGroup); + // TODO: it's more performant to use normal hashTreeRoot() for the rest of the state // this saves ~10ms per ~100ms as monitored on mainnet as of Jul 2024 state.node.rootHashObject; } else { diff --git a/packages/types/src/phase0/viewDU/listValidator.ts b/packages/types/src/phase0/viewDU/listValidator.ts index aa518c4a0208..052ceb93d619 100644 --- a/packages/types/src/phase0/viewDU/listValidator.ts +++ b/packages/types/src/phase0/viewDU/listValidator.ts @@ -96,7 +96,8 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU Date: Fri, 16 Aug 2024 09:54:46 +0700 Subject: [PATCH 09/22] fix: full batch hash on epoch transition (#7030) --- .../chain/blocks/verifyBlocksStateTransitionOnly.ts | 2 +- packages/beacon-node/src/chain/prepareNextSlot.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts index bbb17bfa624a..f8701ecfd0b3 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts @@ -1,3 +1,4 @@ +import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; import { CachedBeaconStateAllForks, stateTransition, @@ -12,7 +13,6 @@ import {BlockProcessOpts} from "../options.js"; import {byteArrayEquals} from "../../util/bytes.js"; import {nextEventLoop} from "../../util/eventLoop.js"; import {BlockInput, ImportBlockOpts} from "./types.js"; -import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; /** * Data in a BeaconBlock is bounded so we can use a single HashComputationGroup for all blocks diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index 48d1d4af81f7..ffb55a3aa2a6 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -1,3 +1,4 @@ +import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; import { computeEpochAtSlot, isExecutionStateType, @@ -18,7 +19,6 @@ import {isQueueErrorAborted} from "../util/queue/index.js"; import {prepareExecutionPayload, getPayloadAttributesForSSE} from "./produceBlock/produceBlockBody.js"; import {IBeaconChain} from "./interface.js"; import {RegenCaller} from "./regen/index.js"; -import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; /* With 12s slot times, this scheduler will run 4s before the start of each slot (`12 / 3 = 4`). */ export const SCHEDULER_LOOKAHEAD_FACTOR = 3; @@ -29,7 +29,7 @@ const PREPARE_EPOCH_LIMIT = 1; /** * The same HashComputationGroup to be used for all epoch transition. */ -const balancesHCGroup = new HashComputationGroup(); +const epochHCGroup = new HashComputationGroup(); /** * At Bellatrix, if we are responsible for proposing in next slot, we want to prepare payload @@ -236,11 +236,7 @@ export class PrepareNextSlotScheduler { source: isEpochTransition ? StateHashTreeRootSource.prepareNextEpoch : StateHashTreeRootSource.prepareNextSlot, }); if (isEpochTransition) { - // balances are completely changed per epoch and it's not much different so we can reuse the HashComputationGroup - state.balances.batchHashTreeRoot(balancesHCGroup); - // TODO: it's more performant to use normal hashTreeRoot() for the rest of the state - // this saves ~10ms per ~100ms as monitored on mainnet as of Jul 2024 - state.node.rootHashObject; + state.batchHashTreeRoot(epochHCGroup); } else { // normal slot, not worth to batch hash state.node.rootHashObject; From 39b6f16f9adb938a483f5174d5db6b8d149cfb6c Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 16 Aug 2024 11:13:36 +0700 Subject: [PATCH 10/22] fix: compute validator root in ListValidatorTreeViewDU --- packages/types/src/phase0/viewDU/listValidator.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/types/src/phase0/viewDU/listValidator.ts b/packages/types/src/phase0/viewDU/listValidator.ts index 052ceb93d619..bb0b61033846 100644 --- a/packages/types/src/phase0/viewDU/listValidator.ts +++ b/packages/types/src/phase0/viewDU/listValidator.ts @@ -40,6 +40,7 @@ const validatorRoots: Uint8Array[] = []; for (let i = 0; i < PARALLEL_FACTOR; i++) { validatorRoots.push(batchLevel3Bytes.subarray(i * 32, (i + 1) * 32)); } +const validatorRoot = new Uint8Array(32); export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU { constructor( @@ -114,8 +115,11 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU Date: Fri, 16 Aug 2024 11:14:29 +0700 Subject: [PATCH 11/22] fix: compute state root in batch in stateTransition() --- .../blocks/verifyBlocksStateTransitionOnly.ts | 1 + .../chain/produceBlock/computeNewStateRoot.ts | 1 + .../src/cache/epochTransitionCache.ts | 7 ++++-- .../epoch/processParticipationFlagUpdates.ts | 2 +- .../state-transition/src/stateTransition.ts | 22 +++++++++++-------- packages/state-transition/src/util/balance.ts | 2 +- 6 files changed, 22 insertions(+), 13 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts index f8701ecfd0b3..d4ae81f800a8 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts @@ -69,6 +69,7 @@ export async function verifyBlocksStateTransitionOnly( const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ source: StateHashTreeRootSource.blockTransition, }); + // state root is computed inside stateTransition(), so it should take no time here const stateRoot = postState.batchHashTreeRoot(blockHCGroup); hashTreeRootTimer?.(); diff --git a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts index bfa30e570e06..483a521401ed 100644 --- a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts +++ b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts @@ -48,6 +48,7 @@ export function computeNewStateRoot( const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ source: StateHashTreeRootSource.computeNewStateRoot, }); + // state root is computed inside stateTransition(), so it should take no time here const newStateRoot = postState.hashTreeRoot(); hashTreeRootTimer?.(); diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 21dbfc1588a2..e09e7f0e06af 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -1,3 +1,4 @@ +import {ReusableListIterator} from "@chainsafe/ssz"; import {Epoch, ValidatorIndex, phase0} from "@lodestar/types"; import {intDiv} from "@lodestar/utils"; import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MAX_EFFECTIVE_BALANCE} from "@lodestar/params"; @@ -16,7 +17,6 @@ import { import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../index.js"; import {computeBaseRewardPerIncrement} from "../util/altair.js"; import {processPendingAttestations} from "../epoch/processPendingAttestations.js"; -import {ReusableListIterator} from "@chainsafe/ssz"; export type EpochTransitionCacheOpts = { /** @@ -332,7 +332,10 @@ export function beforeProcessEpoch( // // Use `else` since indicesEligibleForActivationQueue + indicesEligibleForActivation are mutually exclusive else if (validator.activationEpoch === FAR_FUTURE_EPOCH && validator.activationEligibilityEpoch <= currentEpoch) { - indicesEligibleForActivation.push({validatorIndex: i, activationEligibilityEpoch: validator.activationEligibilityEpoch}); + indicesEligibleForActivation.push({ + validatorIndex: i, + activationEligibilityEpoch: validator.activationEligibilityEpoch, + }); } // To optimize process_registry_updates(): diff --git a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts index 54d609cd1e16..fad5857b3ce6 100644 --- a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts +++ b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts @@ -20,7 +20,7 @@ export function processParticipationFlagUpdates(state: CachedBeaconStateAltair): const currentEpochParticipationNode = ssz.altair.EpochParticipation.tree_setChunksNode( state.currentEpochParticipation.node, zeroNode(ssz.altair.EpochParticipation.chunkDepth), - state.currentEpochParticipation.length, + state.currentEpochParticipation.length ); state.currentEpochParticipation = ssz.altair.EpochParticipation.getViewDU(currentEpochParticipationNode); diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 78bcaa140c62..2f53e861f663 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -1,4 +1,5 @@ import {toHexString} from "@chainsafe/ssz"; +import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; import {SignedBeaconBlock, SignedBlindedBeaconBlock, Slot, ssz} from "@lodestar/types"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {BeaconStateTransitionMetrics, onPostStateMetrics, onStateCloneMetrics} from "./metrics.js"; @@ -55,6 +56,11 @@ export enum StateHashTreeRootSource { computeNewStateRoot = "compute_new_state_root", } +/** + * Data in a BeaconBlock is bounded so we can use a single HashComputationGroup for all blocks + */ +const hcGroup = new HashComputationGroup(); + /** * Implementation Note: follows the optimizations in protolambda's eth2fastspec (https://github.com/protolambda/eth2fastspec) */ @@ -102,9 +108,13 @@ export function stateTransition( processBlock(fork, postState, block, options, options); - const processBlockCommitTimer = metrics?.processBlockCommitTime.startTimer(); - postState.commit(); - processBlockCommitTimer?.(); + // TODO - batch: remove processBlockCommitTime? + const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ + source: StateHashTreeRootSource.stateTransition, + }); + // commit() is done inside batchHashTreeRoot() + const stateRoot = postState.batchHashTreeRoot(hcGroup); + hashTreeRootTimer?.(); // Note: time only on success. Include processBlock and commit processBlockTimer?.(); @@ -115,12 +125,6 @@ export function stateTransition( // Verify state root if (verifyStateRoot) { - const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ - source: StateHashTreeRootSource.stateTransition, - }); - const stateRoot = postState.hashTreeRoot(); - hashTreeRootTimer?.(); - if (!ssz.Root.equals(block.stateRoot, stateRoot)) { throw new Error( `Invalid state root at slot ${block.slot}, expected=${toHexString(block.stateRoot)}, actual=${toHexString( diff --git a/packages/state-transition/src/util/balance.ts b/packages/state-transition/src/util/balance.ts index e14a18ab6606..915b753190b9 100644 --- a/packages/state-transition/src/util/balance.ts +++ b/packages/state-transition/src/util/balance.ts @@ -1,10 +1,10 @@ +import {ReusableListIterator} from "@chainsafe/ssz"; import {EFFECTIVE_BALANCE_INCREMENT} from "@lodestar/params"; import {Gwei, ValidatorIndex, phase0} from "@lodestar/types"; import {bigIntMax} from "@lodestar/utils"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; import {BeaconStateAllForks} from ".."; import {CachedBeaconStateAllForks} from "../types.js"; -import {ReusableListIterator} from "@chainsafe/ssz"; /** * Return the combined effective balance of the [[indices]]. From 064018b5e062d42d188c8960d8f8820e15056c2b Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 19 Aug 2024 08:35:36 +0700 Subject: [PATCH 12/22] fix: correct process_block metric --- packages/state-transition/src/stateTransition.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 2f53e861f663..50b68cf3ea0b 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -108,6 +108,9 @@ export function stateTransition( processBlock(fork, postState, block, options, options); + // Note: time only on success. This does not include hashTreeRoot() time + processBlockTimer?.(); + // TODO - batch: remove processBlockCommitTime? const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ source: StateHashTreeRootSource.stateTransition, @@ -116,8 +119,6 @@ export function stateTransition( const stateRoot = postState.batchHashTreeRoot(hcGroup); hashTreeRootTimer?.(); - // Note: time only on success. Include processBlock and commit - processBlockTimer?.(); if (metrics) { onPostStateMetrics(postState, metrics); From 656d067c58d5fe37b6503ceb4f2763c28672ac72 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 19 Aug 2024 08:37:16 +0700 Subject: [PATCH 13/22] fix: no batch hash in beacon-node block transition, do it in state-transition --- .../src/chain/blocks/verifyBlocksStateTransitionOnly.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts index d4ae81f800a8..49cd46220008 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts @@ -1,4 +1,3 @@ -import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; import { CachedBeaconStateAllForks, stateTransition, @@ -14,11 +13,6 @@ import {byteArrayEquals} from "../../util/bytes.js"; import {nextEventLoop} from "../../util/eventLoop.js"; import {BlockInput, ImportBlockOpts} from "./types.js"; -/** - * Data in a BeaconBlock is bounded so we can use a single HashComputationGroup for all blocks - */ -const blockHCGroup = new HashComputationGroup(); - /** * Verifies 1 or more blocks are fully valid running the full state transition; from a linear sequence of blocks. * @@ -69,8 +63,7 @@ export async function verifyBlocksStateTransitionOnly( const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ source: StateHashTreeRootSource.blockTransition, }); - // state root is computed inside stateTransition(), so it should take no time here - const stateRoot = postState.batchHashTreeRoot(blockHCGroup); + const stateRoot = postState.hashTreeRoot(); hashTreeRootTimer?.(); // Check state root matches From a2542aa01fd98259068e6cb094d342f1cd14b420 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Mon, 19 Aug 2024 11:14:31 +0700 Subject: [PATCH 14/22] chore: add comment --- packages/state-transition/src/stateTransition.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 50b68cf3ea0b..ae543995c881 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -116,6 +116,8 @@ export function stateTransition( source: StateHashTreeRootSource.stateTransition, }); // commit() is done inside batchHashTreeRoot() + // with batchHashTreeRoot(), we're not able to measure commit() time separately + // note that at commit() phase, we batch hash validators via ListValidatorTreeViewDU so this metric is a little bit confusing const stateRoot = postState.batchHashTreeRoot(hcGroup); hashTreeRootTimer?.(); From 8d153437d09a5e3a9c3635095477b56c893c4d7a Mon Sep 17 00:00:00 2001 From: twoeths Date: Mon, 19 Aug 2024 11:40:55 +0700 Subject: [PATCH 15/22] fix: batch hash tree root in state transition (#7032) * fix: compute validator root in ListValidatorTreeViewDU * fix: compute state root in batch in stateTransition() * fix: correct process_block metric * fix: no batch hash in beacon-node block transition, do it in state-transition * chore: add comment --- .../blocks/verifyBlocksStateTransitionOnly.ts | 8 +---- .../chain/produceBlock/computeNewStateRoot.ts | 1 + .../src/cache/epochTransitionCache.ts | 7 +++-- .../epoch/processParticipationFlagUpdates.ts | 2 +- .../state-transition/src/stateTransition.ts | 29 ++++++++++++------- packages/state-transition/src/util/balance.ts | 2 +- .../types/src/phase0/viewDU/listValidator.ts | 6 +++- 7 files changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts index f8701ecfd0b3..49cd46220008 100644 --- a/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts +++ b/packages/beacon-node/src/chain/blocks/verifyBlocksStateTransitionOnly.ts @@ -1,4 +1,3 @@ -import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; import { CachedBeaconStateAllForks, stateTransition, @@ -14,11 +13,6 @@ import {byteArrayEquals} from "../../util/bytes.js"; import {nextEventLoop} from "../../util/eventLoop.js"; import {BlockInput, ImportBlockOpts} from "./types.js"; -/** - * Data in a BeaconBlock is bounded so we can use a single HashComputationGroup for all blocks - */ -const blockHCGroup = new HashComputationGroup(); - /** * Verifies 1 or more blocks are fully valid running the full state transition; from a linear sequence of blocks. * @@ -69,7 +63,7 @@ export async function verifyBlocksStateTransitionOnly( const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ source: StateHashTreeRootSource.blockTransition, }); - const stateRoot = postState.batchHashTreeRoot(blockHCGroup); + const stateRoot = postState.hashTreeRoot(); hashTreeRootTimer?.(); // Check state root matches diff --git a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts index bfa30e570e06..483a521401ed 100644 --- a/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts +++ b/packages/beacon-node/src/chain/produceBlock/computeNewStateRoot.ts @@ -48,6 +48,7 @@ export function computeNewStateRoot( const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ source: StateHashTreeRootSource.computeNewStateRoot, }); + // state root is computed inside stateTransition(), so it should take no time here const newStateRoot = postState.hashTreeRoot(); hashTreeRootTimer?.(); diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 21dbfc1588a2..e09e7f0e06af 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -1,3 +1,4 @@ +import {ReusableListIterator} from "@chainsafe/ssz"; import {Epoch, ValidatorIndex, phase0} from "@lodestar/types"; import {intDiv} from "@lodestar/utils"; import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MAX_EFFECTIVE_BALANCE} from "@lodestar/params"; @@ -16,7 +17,6 @@ import { import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../index.js"; import {computeBaseRewardPerIncrement} from "../util/altair.js"; import {processPendingAttestations} from "../epoch/processPendingAttestations.js"; -import {ReusableListIterator} from "@chainsafe/ssz"; export type EpochTransitionCacheOpts = { /** @@ -332,7 +332,10 @@ export function beforeProcessEpoch( // // Use `else` since indicesEligibleForActivationQueue + indicesEligibleForActivation are mutually exclusive else if (validator.activationEpoch === FAR_FUTURE_EPOCH && validator.activationEligibilityEpoch <= currentEpoch) { - indicesEligibleForActivation.push({validatorIndex: i, activationEligibilityEpoch: validator.activationEligibilityEpoch}); + indicesEligibleForActivation.push({ + validatorIndex: i, + activationEligibilityEpoch: validator.activationEligibilityEpoch, + }); } // To optimize process_registry_updates(): diff --git a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts index 54d609cd1e16..fad5857b3ce6 100644 --- a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts +++ b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts @@ -20,7 +20,7 @@ export function processParticipationFlagUpdates(state: CachedBeaconStateAltair): const currentEpochParticipationNode = ssz.altair.EpochParticipation.tree_setChunksNode( state.currentEpochParticipation.node, zeroNode(ssz.altair.EpochParticipation.chunkDepth), - state.currentEpochParticipation.length, + state.currentEpochParticipation.length ); state.currentEpochParticipation = ssz.altair.EpochParticipation.getViewDU(currentEpochParticipationNode); diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index 78bcaa140c62..ae543995c881 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -1,4 +1,5 @@ import {toHexString} from "@chainsafe/ssz"; +import {HashComputationGroup} from "@chainsafe/persistent-merkle-tree"; import {SignedBeaconBlock, SignedBlindedBeaconBlock, Slot, ssz} from "@lodestar/types"; import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {BeaconStateTransitionMetrics, onPostStateMetrics, onStateCloneMetrics} from "./metrics.js"; @@ -55,6 +56,11 @@ export enum StateHashTreeRootSource { computeNewStateRoot = "compute_new_state_root", } +/** + * Data in a BeaconBlock is bounded so we can use a single HashComputationGroup for all blocks + */ +const hcGroup = new HashComputationGroup(); + /** * Implementation Note: follows the optimizations in protolambda's eth2fastspec (https://github.com/protolambda/eth2fastspec) */ @@ -102,25 +108,26 @@ export function stateTransition( processBlock(fork, postState, block, options, options); - const processBlockCommitTimer = metrics?.processBlockCommitTime.startTimer(); - postState.commit(); - processBlockCommitTimer?.(); - - // Note: time only on success. Include processBlock and commit + // Note: time only on success. This does not include hashTreeRoot() time processBlockTimer?.(); + // TODO - batch: remove processBlockCommitTime? + const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ + source: StateHashTreeRootSource.stateTransition, + }); + // commit() is done inside batchHashTreeRoot() + // with batchHashTreeRoot(), we're not able to measure commit() time separately + // note that at commit() phase, we batch hash validators via ListValidatorTreeViewDU so this metric is a little bit confusing + const stateRoot = postState.batchHashTreeRoot(hcGroup); + hashTreeRootTimer?.(); + + if (metrics) { onPostStateMetrics(postState, metrics); } // Verify state root if (verifyStateRoot) { - const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({ - source: StateHashTreeRootSource.stateTransition, - }); - const stateRoot = postState.hashTreeRoot(); - hashTreeRootTimer?.(); - if (!ssz.Root.equals(block.stateRoot, stateRoot)) { throw new Error( `Invalid state root at slot ${block.slot}, expected=${toHexString(block.stateRoot)}, actual=${toHexString( diff --git a/packages/state-transition/src/util/balance.ts b/packages/state-transition/src/util/balance.ts index e14a18ab6606..915b753190b9 100644 --- a/packages/state-transition/src/util/balance.ts +++ b/packages/state-transition/src/util/balance.ts @@ -1,10 +1,10 @@ +import {ReusableListIterator} from "@chainsafe/ssz"; import {EFFECTIVE_BALANCE_INCREMENT} from "@lodestar/params"; import {Gwei, ValidatorIndex, phase0} from "@lodestar/types"; import {bigIntMax} from "@lodestar/utils"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; import {BeaconStateAllForks} from ".."; import {CachedBeaconStateAllForks} from "../types.js"; -import {ReusableListIterator} from "@chainsafe/ssz"; /** * Return the combined effective balance of the [[indices]]. diff --git a/packages/types/src/phase0/viewDU/listValidator.ts b/packages/types/src/phase0/viewDU/listValidator.ts index 052ceb93d619..bb0b61033846 100644 --- a/packages/types/src/phase0/viewDU/listValidator.ts +++ b/packages/types/src/phase0/viewDU/listValidator.ts @@ -40,6 +40,7 @@ const validatorRoots: Uint8Array[] = []; for (let i = 0; i < PARALLEL_FACTOR; i++) { validatorRoots.push(batchLevel3Bytes.subarray(i * 32, (i + 1) * 32)); } +const validatorRoot = new Uint8Array(32); export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU { constructor( @@ -114,8 +115,11 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU Date: Thu, 22 Aug 2024 16:10:20 +0700 Subject: [PATCH 16/22] fix: handle unmodified validators, ssz PR #397 --- .../types/src/phase0/viewDU/listValidator.ts | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/packages/types/src/phase0/viewDU/listValidator.ts b/packages/types/src/phase0/viewDU/listValidator.ts index bb0b61033846..08d899a90a2a 100644 --- a/packages/types/src/phase0/viewDU/listValidator.ts +++ b/packages/types/src/phase0/viewDU/listValidator.ts @@ -5,7 +5,7 @@ import { ByteViews, ContainerNodeStructTreeViewDU, } from "@chainsafe/ssz"; -import {HashComputationGroup, HashComputationLevel, Node, digestNLevel, setNodesAtDepth} from "@chainsafe/persistent-merkle-tree"; +import {HashComputationLevel, Node, digestNLevel, setNodesAtDepth} from "@chainsafe/persistent-merkle-tree"; import {byteArrayIntoHashObject} from "@chainsafe/as-sha256"; import {ValidatorNodeStructType, ValidatorType, validatorToChunkBytes} from "../validator.js"; @@ -42,6 +42,11 @@ for (let i = 0; i < PARALLEL_FACTOR; i++) { } const validatorRoot = new Uint8Array(32); +/** + * Similar to ListCompositeTreeViewDU with some differences: + * - if called without params, it's from hashTreeRoot() api call, no need to compute root + * - otherwise it's from batchHashTreeRoot() call, compute validator roots in batch + */ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU { constructor( readonly type: ListCompositeType, @@ -52,6 +57,11 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU >; - const indicesChanged = Array.from(this.viewsChanged.keys()).sort((a, b) => a - b); - const endBatch = indicesChanged.length - (indicesChanged.length % PARALLEL_FACTOR); + + const indicesChanged: number[] = []; + for (const [index, viewChanged] of viewsChanged) { + // should not have any params here in order not to compute root + viewChanged.commit(); + // `validators.get(i)` was called but it may not modify any property, do not need to compute root + if (viewChanged.node.h0 === null) { + indicesChanged.push(index); + } + } + + // these validators don't have roots, we compute roots in batch + const sortedIndicesChanged = indicesChanged.sort((a, b) => a - b); + const endBatch = sortedIndicesChanged.length - (sortedIndicesChanged.length % PARALLEL_FACTOR); // nodesChanged is sorted by index const nodesChanged: {index: number; node: Node}[] = []; // commit every 16 validators in batch @@ -77,7 +99,7 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU= 0; j--) { - const viewIndex = indicesChanged[i - j]; + const viewIndex = sortedIndicesChanged[i - j]; const indexInBatch = (i - j) % PARALLEL_FACTOR; const viewChanged = viewsChanged.get(viewIndex); if (viewChanged) { - // should not have any params here in order not to compute root - viewChanged.commit(); const branchNodeStruct = viewChanged.node; byteArrayIntoHashObject(validatorRoots[indexInBatch], 0, branchNodeStruct); nodesChanged.push({index: viewIndex, node: viewChanged.node}); @@ -111,12 +131,10 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU Date: Mon, 26 Aug 2024 18:02:21 +0700 Subject: [PATCH 17/22] fix: separate commit and batch hash validators in listValidator ViewDU --- .../types/src/phase0/viewDU/listValidator.ts | 110 +++++++++--------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/packages/types/src/phase0/viewDU/listValidator.ts b/packages/types/src/phase0/viewDU/listValidator.ts index 08d899a90a2a..05aeeebd75a5 100644 --- a/packages/types/src/phase0/viewDU/listValidator.ts +++ b/packages/types/src/phase0/viewDU/listValidator.ts @@ -8,6 +8,7 @@ import { import {HashComputationLevel, Node, digestNLevel, setNodesAtDepth} from "@chainsafe/persistent-merkle-tree"; import {byteArrayIntoHashObject} from "@chainsafe/as-sha256"; import {ValidatorNodeStructType, ValidatorType, validatorToChunkBytes} from "../validator.js"; +import {ValidatorIndex} from "../../types.js"; /** * hashtree has a MAX_SIZE of 1024 bytes = 32 chunks @@ -81,6 +82,8 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU a - b); - const endBatch = sortedIndicesChanged.length - (sortedIndicesChanged.length % PARALLEL_FACTOR); - // nodesChanged is sorted by index - const nodesChanged: {index: number; node: Node}[] = []; - // commit every 16 validators in batch - for (let i = 0; i < endBatch; i++) { - if (i % PARALLEL_FACTOR === 0) { - batchLevel3Bytes.fill(0); - batchLevel4Bytes.fill(0); - } - const indexInBatch = i % PARALLEL_FACTOR; - const viewIndex = sortedIndicesChanged[i]; - const viewChanged = viewsChanged.get(viewIndex); - if (viewChanged) { - validatorToChunkBytes(level3ByteViewsArr[indexInBatch], level4BytesArr[indexInBatch], viewChanged.value); - } - - if (indexInBatch === PARALLEL_FACTOR - 1) { - // hash level 4, this is populated to pubkeyRoots - digestNLevel(batchLevel4Bytes, 1); - for (let j = 0; j < PARALLEL_FACTOR; j++) { - level3ByteViewsArr[j].uint8Array.set(pubkeyRoots[j], 0); - } - // hash level 3, this is populated to validatorRoots - digestNLevel(batchLevel3Bytes, 3); - // commit all validators in this batch - for (let j = PARALLEL_FACTOR - 1; j >= 0; j--) { - const viewIndex = sortedIndicesChanged[i - j]; - const indexInBatch = (i - j) % PARALLEL_FACTOR; - const viewChanged = viewsChanged.get(viewIndex); - if (viewChanged) { - const branchNodeStruct = viewChanged.node; - byteArrayIntoHashObject(validatorRoots[indexInBatch], 0, branchNodeStruct); - nodesChanged.push({index: viewIndex, node: viewChanged.node}); - // Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal - this.nodes[viewIndex] = viewChanged.node; - } - } - } - } - - // commit the remaining validators, we can do in batch too but don't want to create new Uint8Array views - // it's not much different to commit one by one - for (let i = endBatch; i < sortedIndicesChanged.length; i++) { - const viewIndex = sortedIndicesChanged[i]; - const viewChanged = viewsChanged.get(viewIndex); - if (viewChanged) { - // compute root for each validator - viewChanged.type.hashTreeRootInto(viewChanged.value, validatorRoot, 0); - byteArrayIntoHashObject(validatorRoot, 0, viewChanged.node); - nodesChanged.push({index: viewIndex, node: viewChanged.node}); - // Set new node in nodes array to ensure data represented in the tree and fast nodes access is equal - this.nodes[viewIndex] = viewChanged.node; - } + const nodesChanged: {index: ValidatorIndex; node: Node}[] = new Array(sortedIndicesChanged.length); + for (const [i, validatorIndex] of sortedIndicesChanged.entries()) { + nodesChanged[i] = {index: validatorIndex, node: this.nodes[validatorIndex]}; } + doBatchHashTreeRootValidators(sortedIndicesChanged, viewsChanged); // do the remaining commit step the same to parent (ArrayCompositeTreeViewDU) const indexes = nodesChanged.map((entry) => entry.index); @@ -170,3 +124,53 @@ export class ListValidatorTreeViewDU extends ListCompositeTreeViewDU>): void { + const endBatch = indices.length - (indices.length % PARALLEL_FACTOR); + + // commit every 16 validators in batch + for (let i = 0; i < endBatch; i++) { + if (i % PARALLEL_FACTOR === 0) { + batchLevel3Bytes.fill(0); + batchLevel4Bytes.fill(0); + } + const indexInBatch = i % PARALLEL_FACTOR; + const viewIndex = indices[i]; + const validator = validators.get(viewIndex); + if (validator) { + validatorToChunkBytes(level3ByteViewsArr[indexInBatch], level4BytesArr[indexInBatch], validator.value); + } + + if (indexInBatch === PARALLEL_FACTOR - 1) { + // hash level 4, this is populated to pubkeyRoots + digestNLevel(batchLevel4Bytes, 1); + for (let j = 0; j < PARALLEL_FACTOR; j++) { + level3ByteViewsArr[j].uint8Array.set(pubkeyRoots[j], 0); + } + // hash level 3, this is populated to validatorRoots + digestNLevel(batchLevel3Bytes, 3); + // commit all validators in this batch + for (let j = PARALLEL_FACTOR - 1; j >= 0; j--) { + const viewIndex = indices[i - j]; + const indexInBatch = (i - j) % PARALLEL_FACTOR; + const viewChanged = validators.get(viewIndex); + if (viewChanged) { + const branchNodeStruct = viewChanged.node; + byteArrayIntoHashObject(validatorRoots[indexInBatch], 0, branchNodeStruct); + } + } + } + } + + // commit the remaining validators, we can do in batch too but don't want to create new Uint8Array views + // it's not much different to commit one by one + for (let i = endBatch; i < indices.length; i++) { + const viewIndex = indices[i]; + const viewChanged = validators.get(viewIndex); + if (viewChanged) { + // compute root for each validator + viewChanged.type.hashTreeRootInto(viewChanged.value, validatorRoot, 0); + byteArrayIntoHashObject(validatorRoot, 0, viewChanged.node); + } + } +} From 4e7758866c3d7dbeff0a6cd3aad93d9abc453922 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 29 Aug 2024 17:16:31 +0700 Subject: [PATCH 18/22] fix: use array for validators in beforeProcessEpoch --- .../state-transition/src/cache/epochTransitionCache.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 83aa0b35b100..7e6aea0e92ec 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -1,4 +1,3 @@ -import {ReusableListIterator} from "@chainsafe/ssz"; import {Epoch, ValidatorIndex, phase0} from "@lodestar/types"; import {intDiv} from "@lodestar/utils"; import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE} from "@lodestar/params"; @@ -214,7 +213,7 @@ const nextEpochShufflingActiveValidatorIndices = new Array(); /** * This data is reused and never gc. */ -const validators = new ReusableListIterator(); +const validators: phase0.Validator[] = []; const previousEpochParticipation = new Array(); const currentEpochParticipation = new Array(); @@ -242,9 +241,8 @@ export function beforeProcessEpoch( // To optimize memory each validator node in `state.validators` is represented with a special node type // `BranchNodeStruct` that represents the data as struct internally. This utility grabs the struct data directly // from the nodes without any extra transformation. The returned `validators` array contains native JS objects. - validators.reset(); - state.validators.getAllReadonlyValuesIter(validators); - validators.clean(); + validators.length = state.validators.length; + state.validators.getAllReadonlyValues(validators); const validatorCount = validators.length; From a30e9df6029f9f652d9df2b89bcbde52111e6645 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Thu, 29 Aug 2024 17:16:51 +0700 Subject: [PATCH 19/22] fix: ignore light_client spec tests --- packages/beacon-node/test/spec/utils/specTestIterator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 48a002580043..2911ad52f3fd 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -69,7 +69,7 @@ export const defaultSkipOpts: SkipOpts = { ], // TODO Electra: Review this test in the next spec test release skippedTests: [/^deneb\/light_client\/sync\/.*electra_fork.*/], - skippedRunners: ["merkle_proof", "networking"], + skippedRunners: ["merkle_proof", "networking", "light_client"], }; /** From d5ffae4024b8f65c6589b84aff970a657f37e609 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Wed, 11 Sep 2024 09:56:28 +0700 Subject: [PATCH 20/22] chore: no need to commit() before sliceTo() --- packages/beacon-node/src/db/repositories/depositDataRoot.ts | 1 - packages/beacon-node/src/eth1/utils/deposits.ts | 1 - packages/beacon-node/src/eth1/utils/eth1Data.ts | 1 - packages/state-transition/src/epoch/getRewardsAndPenalties.ts | 2 -- packages/state-transition/src/util/loadState/loadState.ts | 2 -- 5 files changed, 7 deletions(-) diff --git a/packages/beacon-node/src/db/repositories/depositDataRoot.ts b/packages/beacon-node/src/db/repositories/depositDataRoot.ts index e0853680097c..fa8983f0e5fa 100644 --- a/packages/beacon-node/src/db/repositories/depositDataRoot.ts +++ b/packages/beacon-node/src/db/repositories/depositDataRoot.ts @@ -61,7 +61,6 @@ export class DepositDataRootRepository extends Repository { async getDepositRootTreeAtIndex(depositIndex: number): Promise { const depositRootTree = await this.getDepositRootTree(); - depositRootTree.commit(); return depositRootTree.sliceTo(depositIndex); } diff --git a/packages/beacon-node/src/eth1/utils/deposits.ts b/packages/beacon-node/src/eth1/utils/deposits.ts index 392750705527..8d0331fc01d6 100644 --- a/packages/beacon-node/src/eth1/utils/deposits.ts +++ b/packages/beacon-node/src/eth1/utils/deposits.ts @@ -46,7 +46,6 @@ export function getDepositsWithProofs( eth1Data: phase0.Eth1Data ): phase0.Deposit[] { // Get tree at this particular depositCount to compute correct proofs - depositRootTree.commit(); const viewAtDepositCount = depositRootTree.sliceTo(eth1Data.depositCount - 1); const depositRoot = viewAtDepositCount.hashTreeRoot(); diff --git a/packages/beacon-node/src/eth1/utils/eth1Data.ts b/packages/beacon-node/src/eth1/utils/eth1Data.ts index c4244668503f..2b8e976cac2e 100644 --- a/packages/beacon-node/src/eth1/utils/eth1Data.ts +++ b/packages/beacon-node/src/eth1/utils/eth1Data.ts @@ -93,7 +93,6 @@ export function getDepositRootByDepositCount(depositCounts: number[], depositRoo const depositRootByDepositCount = new Map(); for (const depositCount of depositCounts) { - depositRootTree.commit(); depositRootTree = depositRootTree.sliceTo(depositCount - 1); depositRootByDepositCount.set(depositCount, depositRootTree.hashTreeRoot()); } diff --git a/packages/state-transition/src/epoch/getRewardsAndPenalties.ts b/packages/state-transition/src/epoch/getRewardsAndPenalties.ts index f03b55804f6c..cf0a29fd8fe7 100644 --- a/packages/state-transition/src/epoch/getRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/getRewardsAndPenalties.ts @@ -53,8 +53,6 @@ export function getRewardsAndPenaltiesAltair( // TODO: Is there a cheaper way to measure length that going to `state.validators`? const validatorCount = state.validators.length; const activeIncrements = cache.totalActiveStakeByIncrement; - // const rewards = newZeroedArray(validatorCount); - // const penalties = newZeroedArray(validatorCount); rewards.length = validatorCount; rewards.fill(0); penalties.length = validatorCount; diff --git a/packages/state-transition/src/util/loadState/loadState.ts b/packages/state-transition/src/util/loadState/loadState.ts index 9dc32f7bd89f..6e3e9c6719fa 100644 --- a/packages/state-transition/src/util/loadState/loadState.ts +++ b/packages/state-transition/src/util/loadState/loadState.ts @@ -143,7 +143,6 @@ function loadInactivityScores( if (newValidator - 1 < 0) { migratedState.inactivityScores = ssz.altair.InactivityScores.defaultViewDU(); } else { - migratedState.inactivityScores.commit(); migratedState.inactivityScores = migratedState.inactivityScores.sliceTo(newValidator - 1); } } @@ -220,7 +219,6 @@ function loadValidators( modifiedValidators.push(validatorIndex); } } else { - migratedState.validators.commit(); migratedState.validators = migratedState.validators.sliceTo(newValidatorCount - 1); } return modifiedValidators; From 0aeae267b5409eba4f2dbf476c52a71c163c204d Mon Sep 17 00:00:00 2001 From: twoeths Date: Thu, 12 Sep 2024 14:58:36 +0700 Subject: [PATCH 21/22] feat: consume forEach api of ssz (#7079) * fix: remove get*Iter() apis from ssz * feat: decompose validators at epoch transition * fix: improve beforeProcessEpoch() using state.validators.forEachValue() * fix: only populate isCompoundingValidatorArr if electra * fix: use forEach() to improve processEth1Data * fix: use forEach() for getEffectiveBalanceIncrementsZeroInactive() * chore: minimal change of beforeProcessEpoch() compared to te/batch_hash_tree_root --- .../src/block/processEth1Data.ts | 13 ++--- .../src/cache/epochTransitionCache.ts | 48 +++++++++---------- .../epoch/processEffectiveBalanceUpdates.ts | 7 +-- .../state-transition/src/stateTransition.ts | 1 - packages/state-transition/src/util/balance.ts | 20 ++------ 5 files changed, 32 insertions(+), 57 deletions(-) diff --git a/packages/state-transition/src/block/processEth1Data.ts b/packages/state-transition/src/block/processEth1Data.ts index fe40f026028a..5523b1dbb6d9 100644 --- a/packages/state-transition/src/block/processEth1Data.ts +++ b/packages/state-transition/src/block/processEth1Data.ts @@ -1,5 +1,5 @@ import {Node} from "@chainsafe/persistent-merkle-tree"; -import {CompositeViewDU, ReusableListIterator} from "@chainsafe/ssz"; +import {CompositeViewDU} from "@chainsafe/ssz"; import {EPOCHS_PER_ETH1_VOTING_PERIOD, SLOTS_PER_EPOCH} from "@lodestar/params"; import {phase0, ssz} from "@lodestar/types"; import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js"; @@ -23,10 +23,6 @@ export function processEth1Data(state: CachedBeaconStateAllForks, eth1Data: phas state.eth1DataVotes.push(eth1DataView); } -/** - * This data is reused and never gc. - */ -const eth1DataVotes = new ReusableListIterator>(); /** * Returns true if adding the given `eth1Data` to `state.eth1DataVotes` would * result in a change to `state.eth1Data`. @@ -52,14 +48,11 @@ export function becomesNewEth1Data( // Then isEqualEth1DataView compares cached roots (HashObject as of Jan 2022) which is much cheaper // than doing structural equality, which requires tree -> value conversions let sameVotesCount = 0; - eth1DataVotes.reset(); - state.eth1DataVotes.getAllReadonlyIter(eth1DataVotes); - eth1DataVotes.clean(); - for (const eth1DataVote of eth1DataVotes) { + state.eth1DataVotes.forEach((eth1DataVote) => { if (isEqualEth1DataView(eth1DataVote, newEth1Data)) { sameVotesCount++; } - } + }); // The +1 is to account for the `eth1Data` supplied to the function. if ((sameVotesCount + 1) * 2 > SLOTS_PER_ETH1_VOTING_PERIOD) { diff --git a/packages/state-transition/src/cache/epochTransitionCache.ts b/packages/state-transition/src/cache/epochTransitionCache.ts index 7e6aea0e92ec..435291eafe6d 100644 --- a/packages/state-transition/src/cache/epochTransitionCache.ts +++ b/packages/state-transition/src/cache/epochTransitionCache.ts @@ -1,4 +1,4 @@ -import {Epoch, ValidatorIndex, phase0} from "@lodestar/types"; +import {Epoch, ValidatorIndex} from "@lodestar/types"; import {intDiv} from "@lodestar/utils"; import {EPOCHS_PER_SLASHINGS_VECTOR, FAR_FUTURE_EPOCH, ForkSeq, MIN_ACTIVATION_BALANCE} from "@lodestar/params"; @@ -13,7 +13,12 @@ import { FLAG_CURR_TARGET_ATTESTER, FLAG_CURR_HEAD_ATTESTER, } from "../util/attesterStatus.js"; -import {CachedBeaconStateAllForks, CachedBeaconStateAltair, CachedBeaconStatePhase0} from "../index.js"; +import { + CachedBeaconStateAllForks, + CachedBeaconStateAltair, + CachedBeaconStatePhase0, + hasCompoundingWithdrawalCredential, +} from "../index.js"; import {computeBaseRewardPerIncrement} from "../util/altair.js"; import {processPendingAttestations} from "../epoch/processPendingAttestations.js"; @@ -127,11 +132,7 @@ export interface EpochTransitionCache { flags: number[]; - /** - * Validators in the current epoch, should use it for read-only value instead of accessing state.validators directly. - * Note that during epoch processing, validators could be updated so need to use it with care. - */ - validators: phase0.Validator[]; + isCompoundingValidatorArr: boolean[]; /** * This is for electra only @@ -210,10 +211,9 @@ const inclusionDelays = new Array(); const flags = new Array(); /** WARNING: reused, never gc'd */ const nextEpochShufflingActiveValidatorIndices = new Array(); -/** - * This data is reused and never gc. - */ -const validators: phase0.Validator[] = []; +/** WARNING: reused, never gc'd */ +const isCompoundingValidatorArr = new Array(); + const previousEpochParticipation = new Array(); const currentEpochParticipation = new Array(); @@ -237,15 +237,10 @@ export function beforeProcessEpoch( const indicesToEject: ValidatorIndex[] = []; let totalActiveStakeByIncrement = 0; - - // To optimize memory each validator node in `state.validators` is represented with a special node type - // `BranchNodeStruct` that represents the data as struct internally. This utility grabs the struct data directly - // from the nodes without any extra transformation. The returned `validators` array contains native JS objects. - validators.length = state.validators.length; - state.validators.getAllReadonlyValues(validators); - - const validatorCount = validators.length; - + const validatorCount = state.validators.length; + if (forkSeq >= ForkSeq.electra) { + isCompoundingValidatorArr.length = validatorCount; + } nextEpochShufflingActiveValidatorIndices.length = validatorCount; let nextEpochShufflingActiveIndicesLength = 0; // pre-fill with true (most validators are active) @@ -275,11 +270,13 @@ export function beforeProcessEpoch( const effectiveBalancesByIncrements = epochCtx.effectiveBalanceIncrements; - // for (let i = 0; i < validatorCount; i++) { - let i = 0; - for (const validator of validators) { + state.validators.forEachValue((validator, i) => { let flag = 0; + if (forkSeq >= ForkSeq.electra) { + isCompoundingValidatorArr[i] = hasCompoundingWithdrawalCredential(validator.withdrawalCredentials); + } + if (validator.slashed) { if (slashingsEpoch === validator.withdrawableEpoch) { indicesToSlash.push(i); @@ -370,8 +367,7 @@ export function beforeProcessEpoch( if (isActiveNext2) { nextEpochShufflingActiveValidatorIndices[nextEpochShufflingActiveIndicesLength++] = i; } - i++; - } + }); if (totalActiveStakeByIncrement < 1) { totalActiveStakeByIncrement = 1; @@ -508,7 +504,7 @@ export function beforeProcessEpoch( proposerIndices, inclusionDelays, flags, - validators, + isCompoundingValidatorArr, // will be assigned in processPendingConsolidations() newCompoundingValidators: undefined, // Will be assigned in processRewardsAndPenalties() diff --git a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts index 0ea4b49dddf4..9203c0419a95 100644 --- a/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts +++ b/packages/state-transition/src/epoch/processEffectiveBalanceUpdates.ts @@ -10,7 +10,6 @@ import { TIMELY_TARGET_FLAG_INDEX, } from "@lodestar/params"; import {EpochTransitionCache, CachedBeaconStateAllForks, BeaconStateAltair} from "../types.js"; -import {hasCompoundingWithdrawalCredential} from "../util/electra.js"; /** Same to https://github.com/ethereum/eth2.0-specs/blob/v1.1.0-alpha.5/specs/altair/beacon-chain.md#has_flag */ const TIMELY_TARGET = 1 << TIMELY_TARGET_FLAG_INDEX; @@ -45,7 +44,7 @@ export function processEffectiveBalanceUpdates( // and updated in processPendingBalanceDeposits() and processPendingConsolidations() // so it's recycled here for performance. const balances = cache.balances ?? state.balances.getAll(); - const currentEpochValidators = cache.validators; + const {isCompoundingValidatorArr} = cache; const newCompoundingValidators = cache.newCompoundingValidators ?? new Set(); let numUpdate = 0; @@ -61,9 +60,7 @@ export function processEffectiveBalanceUpdates( effectiveBalanceLimit = MAX_EFFECTIVE_BALANCE; } else { // from electra, effectiveBalanceLimit is per validator - const isCompoundingValidator = - hasCompoundingWithdrawalCredential(currentEpochValidators[i].withdrawalCredentials) || - newCompoundingValidators.has(i); + const isCompoundingValidator = isCompoundingValidatorArr[i] || newCompoundingValidators.has(i); effectiveBalanceLimit = isCompoundingValidator ? MAX_EFFECTIVE_BALANCE_ELECTRA : MIN_ACTIVATION_BALANCE; } diff --git a/packages/state-transition/src/stateTransition.ts b/packages/state-transition/src/stateTransition.ts index dcbb8067d94f..171ef089b5a5 100644 --- a/packages/state-transition/src/stateTransition.ts +++ b/packages/state-transition/src/stateTransition.ts @@ -124,7 +124,6 @@ export function stateTransition( const stateRoot = postState.batchHashTreeRoot(hcGroup); hashTreeRootTimer?.(); - if (metrics) { onPostStateMetrics(postState, metrics); } diff --git a/packages/state-transition/src/util/balance.ts b/packages/state-transition/src/util/balance.ts index eca6372c220c..a1b086cbd591 100644 --- a/packages/state-transition/src/util/balance.ts +++ b/packages/state-transition/src/util/balance.ts @@ -1,6 +1,5 @@ -import {ReusableListIterator} from "@chainsafe/ssz"; import {EFFECTIVE_BALANCE_INCREMENT} from "@lodestar/params"; -import {Gwei, ValidatorIndex, phase0} from "@lodestar/types"; +import {Gwei, ValidatorIndex} from "@lodestar/types"; import {bigIntMax} from "@lodestar/utils"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; import {BeaconStateAllForks} from ".."; @@ -44,11 +43,6 @@ export function decreaseBalance(state: BeaconStateAllForks, index: ValidatorInde state.balances.set(index, Math.max(0, newBalance)); } -/** - * This data is reused and never gc. - */ -const validators = new ReusableListIterator(); - /** * This method is used to get justified balances from a justified state. * This is consumed by forkchoice which based on delta so we return "by increment" (in ether) value, @@ -69,16 +63,13 @@ export function getEffectiveBalanceIncrementsZeroInactive( validatorCount ); - validators.reset(); - justifiedState.validators.getAllReadonlyValuesIter(validators); - validators.clean(); - let i = 0; let j = 0; - for (const validator of validators) { + justifiedState.validators.forEachValue((validator, i) => { + const {slashed} = validator; if (i === activeIndices[j]) { // active validator j++; - if (validator.slashed) { + if (slashed) { // slashed validator effectiveBalanceIncrementsZeroInactive[i] = 0; } @@ -86,8 +77,7 @@ export function getEffectiveBalanceIncrementsZeroInactive( // inactive validator effectiveBalanceIncrementsZeroInactive[i] = 0; } - i++; - } + }); return effectiveBalanceIncrementsZeroInactive; } From 604a2a75c53e8dfa5340b1bc414c5d86d1c241c0 Mon Sep 17 00:00:00 2001 From: twoeths Date: Sat, 14 Sep 2024 15:18:53 +0700 Subject: [PATCH 22/22] feat: implement BalancesTreeCache (#7084) * feat: implement balances tree cache * fix: set balancesTreeCache when clone EpochCache --- .../beacon-node/src/chain/archiver/index.ts | 2 +- .../src/chain/balancesTreeCache.ts | 38 +++++++++++++++ .../src/chain/blocks/importBlock.ts | 11 ++++- packages/beacon-node/src/chain/chain.ts | 14 ++++++ packages/beacon-node/src/chain/interface.ts | 2 + .../beacon-node/src/chain/regen/queued.ts | 22 ++++++--- .../stateCache/inMemoryCheckpointsCache.ts | 25 +++++++--- .../stateCache/persistentCheckpointsCache.ts | 47 ++++++++++++------- .../beacon-node/src/chain/stateCache/types.ts | 7 ++- .../src/metrics/metrics/lodestar.ts | 15 ++++++ .../src/cache/balancesTreeCache.ts | 5 ++ .../state-transition/src/cache/epochCache.ts | 10 +++- .../src/epoch/processRewardsAndPenalties.ts | 2 +- packages/state-transition/src/index.ts | 1 + 14 files changed, 166 insertions(+), 35 deletions(-) create mode 100644 packages/beacon-node/src/chain/balancesTreeCache.ts create mode 100644 packages/state-transition/src/cache/balancesTreeCache.ts diff --git a/packages/beacon-node/src/chain/archiver/index.ts b/packages/beacon-node/src/chain/archiver/index.ts index 294c2281e19b..e2f55b8f0500 100644 --- a/packages/beacon-node/src/chain/archiver/index.ts +++ b/packages/beacon-node/src/chain/archiver/index.ts @@ -107,7 +107,7 @@ export class Archiver { // should be after ArchiveBlocksTask to handle restart cleanly await this.statesArchiver.maybeArchiveState(finalized); - this.chain.regen.pruneOnFinalized(finalizedEpoch); + this.chain.pruneOnFinalized(finalizedEpoch); // tasks rely on extended fork choice const prunedBlocks = this.chain.forkChoice.prune(finalized.rootHex); diff --git a/packages/beacon-node/src/chain/balancesTreeCache.ts b/packages/beacon-node/src/chain/balancesTreeCache.ts new file mode 100644 index 000000000000..462ae860809e --- /dev/null +++ b/packages/beacon-node/src/chain/balancesTreeCache.ts @@ -0,0 +1,38 @@ +import {ListBasicTreeViewDU, UintNumberType} from "@chainsafe/ssz"; +import {IBalancesTreeCache, CachedBeaconStateAllForks} from "@lodestar/state-transition"; +import {Metrics} from "../metrics/index.js"; + +const MAX_ITEMS = 2; + +export class BalancesTreeCache implements IBalancesTreeCache { + private readonly unusedBalancesTrees: ListBasicTreeViewDU[] = []; + + constructor(private readonly metrics: Metrics | null = null) { + if (metrics) { + metrics.balancesTreeCache.size.addCollect(() => { + metrics.balancesTreeCache.size.set(this.unusedBalancesTrees.length); + }); + } + } + + processUnusedState(state: CachedBeaconStateAllForks | undefined): void { + if (state === undefined) { + return; + } + + this.unusedBalancesTrees.push(state.balances); + while (this.unusedBalancesTrees.length > MAX_ITEMS) { + this.unusedBalancesTrees.shift(); + } + } + + getUnusedBalances(): ListBasicTreeViewDU | undefined { + if (this.unusedBalancesTrees.length === 0) { + this.metrics?.balancesTreeCache.miss.inc(); + return undefined; + } + + this.metrics?.balancesTreeCache.hit.inc(); + return this.unusedBalancesTrees.shift(); + } +} diff --git a/packages/beacon-node/src/chain/blocks/importBlock.ts b/packages/beacon-node/src/chain/blocks/importBlock.ts index de5ecf607d95..1d1772a1692f 100644 --- a/packages/beacon-node/src/chain/blocks/importBlock.ts +++ b/packages/beacon-node/src/chain/blocks/importBlock.ts @@ -97,7 +97,16 @@ export async function importBlock( // This adds the state necessary to process the next block // Some block event handlers require state being in state cache so need to do this before emitting EventType.block - this.regen.processState(blockRootHex, postState); + this.regen.processState(blockRootHex, postState).then((prunedStates) => { + if (prunedStates) { + for (const states of prunedStates.values()) { + // cp states on the same epoch shares the same balances seed tree so only need one of them + this.balancesTreeCache.processUnusedState(states[0]); + } + } + }).catch((e) => { + this.logger.error("Regen error to process state for block", {slot: blockSlot, root: blockRootHex}, e as Error); + }); this.metrics?.importBlock.bySource.inc({source}); this.logger.verbose("Added block to forkchoice and state cache", {slot: blockSlot, root: blockRootHex}); diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index 8dbb49798538..69d6a6b41c56 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -101,6 +101,7 @@ import {DbCPStateDatastore} from "./stateCache/datastore/db.js"; import {FileCPStateDatastore} from "./stateCache/datastore/file.js"; import {SyncCommitteeRewards, computeSyncCommitteeRewards} from "./rewards/syncCommitteeRewards.js"; import {AttestationsRewards, computeAttestationsRewards} from "./rewards/attestationsRewards.js"; +import {BalancesTreeCache} from "./balancesTreeCache.js"; /** * Arbitrary constants, blobs and payloads should be consumed immediately in the same slot @@ -158,6 +159,7 @@ export class BeaconChain implements IBeaconChain { readonly beaconProposerCache: BeaconProposerCache; readonly checkpointBalancesCache: CheckpointBalancesCache; readonly shufflingCache: ShufflingCache; + readonly balancesTreeCache: BalancesTreeCache; /** Map keyed by executionPayload.blockHash of the block for those blobs */ readonly producedContentsCache = new Map(); @@ -247,6 +249,7 @@ export class BeaconChain implements IBeaconChain { this.beaconProposerCache = new BeaconProposerCache(opts); this.checkpointBalancesCache = new CheckpointBalancesCache(); this.shufflingCache = new ShufflingCache(metrics, this.opts); + this.balancesTreeCache = new BalancesTreeCache(metrics); // Restore state caches // anchorState may already by a CachedBeaconState. If so, don't create the cache again, since deserializing all @@ -260,6 +263,7 @@ export class BeaconChain implements IBeaconChain { config, pubkey2index: new PubkeyIndexMap(), index2pubkey: [], + balancesTreeCache: this.balancesTreeCache, }); this.shufflingCache.processState(cachedState, cachedState.epochCtx.previousShuffling.epoch); this.shufflingCache.processState(cachedState, cachedState.epochCtx.currentShuffling.epoch); @@ -863,6 +867,16 @@ export class BeaconChain implements IBeaconChain { } } + pruneOnFinalized(finalizedEpoch: Epoch): void { + const prunedStates = this.regen.pruneOnFinalized(finalizedEpoch); + if (prunedStates) { + // cp states on the same epoch shares the same balances seed tree so only need one of them + for (const states of prunedStates.values()) { + this.balancesTreeCache.processUnusedState(states[0]); + } + } + } + /** * Regenerate state for attestation verification, this does not happen with default chain option of maxSkipSlots = 32 . * However, need to handle just in case. Lodestar doesn't support multiple regen state requests for attestation verification diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index 5185662eaa4f..e70d5a0c8297 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -241,6 +241,8 @@ export interface IBeaconChain { blockRef: BeaconBlock | BlindedBeaconBlock, validatorIds?: (ValidatorIndex | string)[] ): Promise; + + pruneOnFinalized(finalizedEpoch: Epoch): void; } export type SSZObjectType = diff --git a/packages/beacon-node/src/chain/regen/queued.ts b/packages/beacon-node/src/chain/regen/queued.ts index 57e64bd364ea..45b8b4f76c5f 100644 --- a/packages/beacon-node/src/chain/regen/queued.ts +++ b/packages/beacon-node/src/chain/regen/queued.ts @@ -148,16 +148,26 @@ export class QueuedStateRegenerator implements IStateRegenerator { this.blockStateCache.prune(headStateRoot); } - pruneOnFinalized(finalizedEpoch: number): void { - this.checkpointStateCache.pruneFinalized(finalizedEpoch); + pruneOnFinalized(finalizedEpoch: number): Map | null { + const prunedStates = this.checkpointStateCache.pruneFinalized(finalizedEpoch); this.blockStateCache.deleteAllBeforeEpoch(finalizedEpoch); + + return prunedStates; } - processState(blockRootHex: RootHex, postState: CachedBeaconStateAllForks): void { + async processState( + blockRootHex: RootHex, + postState: CachedBeaconStateAllForks + ): Promise | null> { this.blockStateCache.add(postState); - this.checkpointStateCache.processState(blockRootHex, postState).catch((e) => { - this.logger.debug("Error processing block state", {blockRootHex, slot: postState.slot}, e); - }); + let prunedStates: Map | null = null; + try { + prunedStates = await this.checkpointStateCache.processState(blockRootHex, postState); + } catch (e) { + this.logger.debug("Error processing block state", {blockRootHex, slot: postState.slot}, e as Error); + } + + return prunedStates; } addCheckpointState(cp: phase0.Checkpoint, item: CachedBeaconStateAllForks): void { diff --git a/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts b/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts index 38aeabb97955..bb1ff18e25de 100644 --- a/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts +++ b/packages/beacon-node/src/chain/stateCache/inMemoryCheckpointsCache.ts @@ -59,9 +59,9 @@ export class InMemoryCheckpointStateCache implements CheckpointStateCache { return this.getLatest(rootHex, maxEpoch, opts); } - async processState(): Promise { + async processState(): Promise | null> { // do nothing, this class does not support prunning - return 0; + return null; } get(cp: CheckpointHex, opts?: StateCloneOpts): CachedBeaconStateAllForks | null { @@ -122,12 +122,17 @@ export class InMemoryCheckpointStateCache implements CheckpointStateCache { return previousHits; } - pruneFinalized(finalizedEpoch: Epoch): void { + pruneFinalized(finalizedEpoch: Epoch): Map { + const result = new Map(); + for (const epoch of this.epochIndex.keys()) { if (epoch < finalizedEpoch) { - this.deleteAllEpochItems(epoch); + const deletedStates = this.deleteAllEpochItems(epoch); + result.set(epoch, deletedStates); } } + + return result; } prune(finalizedEpoch: Epoch, justifiedEpoch: Epoch): void { @@ -153,11 +158,19 @@ export class InMemoryCheckpointStateCache implements CheckpointStateCache { } } - deleteAllEpochItems(epoch: Epoch): void { + deleteAllEpochItems(epoch: Epoch): CachedBeaconStateAllForks[] { + const states = []; for (const rootHex of this.epochIndex.get(epoch) || []) { - this.cache.delete(toCheckpointKey({rootHex, epoch})); + const key = toCheckpointKey({rootHex, epoch}); + const state = this.cache.get(key); + if (state) { + states.push(state); + } + this.cache.delete(key); } this.epochIndex.delete(epoch); + + return states; } clear(): void { diff --git a/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts b/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts index 190b79e58cd6..b4d16ae1d500 100644 --- a/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts +++ b/packages/beacon-node/src/chain/stateCache/persistentCheckpointsCache.ts @@ -421,7 +421,7 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { /** * Prune all checkpoint states before the provided finalized epoch. */ - pruneFinalized(finalizedEpoch: Epoch): void { + pruneFinalized(finalizedEpoch: Epoch): Map | null { for (const epoch of this.epochIndex.keys()) { if (epoch < finalizedEpoch) { this.deleteAllEpochItems(epoch).catch((e) => @@ -429,6 +429,9 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { ); } } + + // not likely to return anything in-memory state because we may persist states even before they are finalized + return null; } /** @@ -481,12 +484,14 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { * * As of Mar 2024, it takes <=350ms to persist a holesky state on fast server */ - async processState(blockRootHex: RootHex, state: CachedBeaconStateAllForks): Promise { - let persistCount = 0; + async processState( + blockRootHex: RootHex, + state: CachedBeaconStateAllForks + ): Promise | null> { // it's important to sort the epochs in ascending order, in case of big reorg we always want to keep the most recent checkpoint states const sortedEpochs = Array.from(this.epochIndex.keys()).sort((a, b) => a - b); if (sortedEpochs.length <= this.maxEpochsInMemory) { - return 0; + return null; } const blockSlot = state.slot; @@ -502,24 +507,19 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { // normally the block persist happens at 2/3 of slot 0 of epoch, if it's already late then just skip to allow other tasks to run // there are plenty of chances in the same epoch to persist checkpoint states, also if block is late it could be reorged this.logger.verbose("Skip persist checkpoint states", {blockSlot, root: blockRootHex}); - return 0; + return null; } const persistEpochs = sortedEpochs.slice(0, sortedEpochs.length - this.maxEpochsInMemory); + + const result = new Map(); for (const lowestEpoch of persistEpochs) { // usually there is only 0 or 1 epoch to persist in this loop - persistCount += await this.processPastEpoch(blockRootHex, state, lowestEpoch); + const prunedStates = await this.processPastEpoch(blockRootHex, state, lowestEpoch); + result.set(lowestEpoch, prunedStates); } - if (persistCount > 0) { - this.logger.verbose("Persisted checkpoint states", { - slot: blockSlot, - root: blockRootHex, - persistCount, - persistEpochs: persistEpochs.length, - }); - } - return persistCount; + return result; } /** @@ -648,13 +648,16 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { * Performance note: * - In normal condition, we persist 1 checkpoint state per epoch. * - In reorged condition, we may persist multiple (most likely 2) checkpoint states per epoch. + * + * Return the pruned states from memory */ private async processPastEpoch( blockRootHex: RootHex, state: CachedBeaconStateAllForks, epoch: Epoch - ): Promise { + ): Promise { let persistCount = 0; + const prunedStates: CachedBeaconStateAllForks[] = []; const epochBoundarySlot = computeStartSlotAtEpoch(epoch); const epochBoundaryRoot = epochBoundarySlot === state.slot ? fromHexString(blockRootHex) : getBlockRootAtSlot(state, epochBoundarySlot); @@ -735,10 +738,20 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache { this.metrics?.statePruneFromMemoryCount.inc(); this.logger.verbose("Pruned checkpoint state from memory", logMeta); } + + prunedStates.push(state); } } - return persistCount; + if (persistCount > 0) { + this.logger.verbose("Persisted checkpoint states", { + stateSlot: state.slot, + blockRoot: blockRootHex, + persistCount, + }); + } + + return prunedStates; } /** diff --git a/packages/beacon-node/src/chain/stateCache/types.ts b/packages/beacon-node/src/chain/stateCache/types.ts index 1e8d6bd1bd62..cd93c34bde89 100644 --- a/packages/beacon-node/src/chain/stateCache/types.ts +++ b/packages/beacon-node/src/chain/stateCache/types.ts @@ -72,8 +72,11 @@ export interface CheckpointStateCache { ): Promise; updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null; prune(finalizedEpoch: Epoch, justifiedEpoch: Epoch): void; - pruneFinalized(finalizedEpoch: Epoch): void; - processState(blockRootHex: RootHex, state: CachedBeaconStateAllForks): Promise; + pruneFinalized(finalizedEpoch: Epoch): Map | null; + processState( + blockRootHex: RootHex, + state: CachedBeaconStateAllForks + ): Promise | null>; clear(): void; dumpSummary(): routes.lodestar.StateCacheItem[]; /** Expose beacon states stored in cache. Use with caution */ diff --git a/packages/beacon-node/src/metrics/metrics/lodestar.ts b/packages/beacon-node/src/metrics/metrics/lodestar.ts index 737a900e5f64..6ade99eb7b6a 100644 --- a/packages/beacon-node/src/metrics/metrics/lodestar.ts +++ b/packages/beacon-node/src/metrics/metrics/lodestar.ts @@ -1315,6 +1315,21 @@ export function createLodestarMetrics( }), }, + balancesTreeCache: { + size: register.gauge({ + name: "lodestar_balances_tree_cache_size", + help: "Balances tree cache size", + }), + hit: register.gauge({ + name: "lodestar_balances_tree_cache_hit_total", + help: "Total number of balances tree cache hits", + }), + miss: register.gauge({ + name: "lodestar_balances_tree_cache_miss_total", + help: "Total number of balances tree cache misses", + }), + }, + seenCache: { aggregatedAttestations: { superSetCheckTotal: register.histogram({ diff --git a/packages/state-transition/src/cache/balancesTreeCache.ts b/packages/state-transition/src/cache/balancesTreeCache.ts new file mode 100644 index 000000000000..0466824e490d --- /dev/null +++ b/packages/state-transition/src/cache/balancesTreeCache.ts @@ -0,0 +1,5 @@ +import {UintNumberType, ListBasicTreeViewDU} from "@chainsafe/ssz"; + +export interface IBalancesTreeCache { + getUnusedBalances(): ListBasicTreeViewDU | undefined; +} diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index af6e976e9089..4ad5be709bf9 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -60,6 +60,7 @@ import { SyncCommitteeCache, SyncCommitteeCacheEmpty, } from "./syncCommitteeCache.js"; +import {IBalancesTreeCache} from "./balancesTreeCache.js"; /** `= PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)` */ export const PROPOSER_WEIGHT_FACTOR = PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT); @@ -68,6 +69,7 @@ export type EpochCacheImmutableData = { config: BeaconConfig; pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; + balancesTreeCache?: IBalancesTreeCache; }; export type EpochCacheOpts = { @@ -129,6 +131,8 @@ export class EpochCache { */ unfinalizedPubkey2index: UnfinalizedPubkeyIndexMap; + balancesTreeCache?: IBalancesTreeCache; + /** * Indexes of the block proposers for the current epoch. * @@ -245,6 +249,7 @@ export class EpochCache { pubkey2index: PubkeyIndexMap; index2pubkey: Index2PubkeyCache; unfinalizedPubkey2index: UnfinalizedPubkeyIndexMap; + balancesTreeCache?: IBalancesTreeCache; proposers: number[]; proposersPrevEpoch: number[] | null; proposersNextEpoch: ProposersDeferred; @@ -273,6 +278,7 @@ export class EpochCache { this.pubkey2index = data.pubkey2index; this.index2pubkey = data.index2pubkey; this.unfinalizedPubkey2index = data.unfinalizedPubkey2index; + this.balancesTreeCache = data.balancesTreeCache; this.proposers = data.proposers; this.proposersPrevEpoch = data.proposersPrevEpoch; this.proposersNextEpoch = data.proposersNextEpoch; @@ -306,7 +312,7 @@ export class EpochCache { */ static createFromState( state: BeaconStateAllForks, - {config, pubkey2index, index2pubkey}: EpochCacheImmutableData, + {config, pubkey2index, index2pubkey, balancesTreeCache}: EpochCacheImmutableData, opts?: EpochCacheOpts ): EpochCache { const currentEpoch = computeEpochAtSlot(state.slot); @@ -483,6 +489,7 @@ export class EpochCache { index2pubkey, // `createFromFinalizedState()` creates cache with empty unfinalizedPubkey2index. Be cautious to only pass in finalized state unfinalizedPubkey2index: newUnfinalizedPubkeyIndexMap(), + balancesTreeCache, proposers, // On first epoch, set to null to prevent unnecessary work since this is only used for metrics proposersPrevEpoch: null, @@ -524,6 +531,7 @@ export class EpochCache { index2pubkey: this.index2pubkey, // No need to clone this reference. On each mutation the `unfinalizedPubkey2index` reference is replaced, @see `addPubkey` unfinalizedPubkey2index: this.unfinalizedPubkey2index, + balancesTreeCache: this.balancesTreeCache, // Immutable data proposers: this.proposers, proposersPrevEpoch: this.proposersPrevEpoch, diff --git a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts index ef074dfd6820..5b42f4175a04 100644 --- a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts @@ -39,7 +39,7 @@ export function processRewardsAndPenalties( // important: do not change state one balance at a time. Set them all at once, constructing the tree in one go // cache the balances array, too - state.balances = ssz.phase0.Balances.toViewDU(balances); + state.balances = ssz.phase0.Balances.toViewDU(balances, state.epochCtx.balancesTreeCache?.getUnusedBalances()); // For processEffectiveBalanceUpdates() to prevent having to re-compute the balances array. // For validator metrics diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index 4ed801e3c490..7fcc5a6c860a 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -42,6 +42,7 @@ export { EpochCacheErrorCode, } from "./cache/epochCache.js"; export {type EpochTransitionCache, beforeProcessEpoch} from "./cache/epochTransitionCache.js"; +export type {IBalancesTreeCache} from "./cache/balancesTreeCache.js"; // Aux data-structures export {