From 5c40060322eb6d4768e68e18bd806d2e481d177d Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 17 Jul 2026 15:06:41 +0700 Subject: [PATCH 1/3] feat: more progressive ssz utils to reuse nodes --- .../epoch/processParticipationFlagUpdates.ts | 9 ++- .../src/slot/upgradeStateToGloas.ts | 73 ++++++++++++++----- packages/state-transition/src/util/index.ts | 1 + packages/state-transition/src/util/ssz.ts | 42 +++++++++++ .../processParticipationFlagUpdates.test.ts | 19 +++++ .../test/unit/upgradeState.test.ts | 27 +++++-- .../test/unit/util/ssz.test.ts | 59 +++++++++++++++ 7 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 packages/state-transition/src/util/ssz.ts create mode 100644 packages/state-transition/test/unit/util/ssz.test.ts diff --git a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts index ae917dd5ecb2..05556342fedd 100644 --- a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts +++ b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts @@ -2,6 +2,7 @@ import {zeroNode} from "@chainsafe/persistent-merkle-tree"; import {ssz} from "@lodestar/types"; import type {CachedBeaconStateAltair, CachedBeaconStateGloas} from "../types.js"; import {isGloasStateType} from "../util/execution.js"; +import {zeroProgressiveListBasicRootNode} from "../util/ssz.js"; /** * Updates `state.previousEpochParticipation` with precalculated epoch participation. Creates a new empty tree for @@ -34,7 +35,11 @@ export function processParticipationFlagUpdates(state: CachedBeaconStateAltair): function processParticipationFlagUpdatesGloas(state: CachedBeaconStateGloas): void { state.previousEpochParticipation = state.currentEpochParticipation; - state.currentEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( - new Array(state.currentEpochParticipation.length).fill(0) + + // Same trick as the altair path above, adapted to the progressive-list tree shape: all chunks + // are zero so the chunks tree is a chain of pre-computed zeroNodes, built in O(log n) instead + // of re-merkleizing a validator-count-sized array every epoch. + state.currentEpochParticipation = ssz.gloas.EpochParticipation.getViewDU( + zeroProgressiveListBasicRootNode(ssz.gloas.EpochParticipation.itemsPerChunk, state.currentEpochParticipation.length) ); } diff --git a/packages/state-transition/src/slot/upgradeStateToGloas.ts b/packages/state-transition/src/slot/upgradeStateToGloas.ts index 7b1db786d798..08e288df77bc 100644 --- a/packages/state-transition/src/slot/upgradeStateToGloas.ts +++ b/packages/state-transition/src/slot/upgradeStateToGloas.ts @@ -1,5 +1,15 @@ -import {BranchNode, LeafNode, Node} from "@chainsafe/persistent-merkle-tree"; -import {progressiveSubtreeFillToContents} from "@chainsafe/ssz"; +import {getNodesAtDepth} from "@chainsafe/persistent-merkle-tree"; +import { + BasicType, + CompositeType, + CompositeView, + CompositeViewDU, + ListBasicTreeViewDU, + ListCompositeTreeViewDU, + ProgressiveListBasicType, + ProgressiveListCompositeType, + ValueOf, +} from "@chainsafe/ssz"; import {PAYLOAD_BUILDER_VERSION, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; import {ssz} from "@lodestar/types"; import {toPubkeyHex} from "@lodestar/utils"; @@ -9,6 +19,7 @@ import {CachedBeaconStateFulu, CachedBeaconStateGloas} from "../types.js"; import {addBuilderToRegistry, initializePtcWindow, isBuilderWithdrawalCredential} from "../util/gloas.js"; import {isValidatorKnown} from "../util/index.js"; import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js"; +import {progressiveListRootNode} from "../util/ssz.js"; /** * Upgrade a state from Fulu to Gloas. @@ -37,20 +48,25 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea stateGloasView.eth1DataVotes = stateGloasCloned.eth1DataVotes; stateGloasView.eth1DepositIndex = stateGloasCloned.eth1DepositIndex; stateGloasView.validators = migrateCompositeListToGloas(stateGloasCloned.validators, ssz.gloas.Validators); - stateGloasView.balances = ssz.gloas.Balances.toViewDU(stateGloasCloned.balances.getAll()); + stateGloasView.balances = migrateBasicListToGloas(stateGloasCloned.balances, ssz.gloas.Balances); stateGloasView.randaoMixes = stateGloasCloned.randaoMixes; stateGloasView.slashings = stateGloasCloned.slashings; - stateGloasView.previousEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( - stateGloasCloned.previousEpochParticipation.getAll() + stateGloasView.previousEpochParticipation = migrateBasicListToGloas( + stateGloasCloned.previousEpochParticipation, + ssz.gloas.EpochParticipation ); - stateGloasView.currentEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( - stateGloasCloned.currentEpochParticipation.getAll() + stateGloasView.currentEpochParticipation = migrateBasicListToGloas( + stateGloasCloned.currentEpochParticipation, + ssz.gloas.EpochParticipation ); stateGloasView.justificationBits = stateGloasCloned.justificationBits; stateGloasView.previousJustifiedCheckpoint = stateGloasCloned.previousJustifiedCheckpoint; stateGloasView.currentJustifiedCheckpoint = stateGloasCloned.currentJustifiedCheckpoint; stateGloasView.finalizedCheckpoint = stateGloasCloned.finalizedCheckpoint; - stateGloasView.inactivityScores = ssz.gloas.InactivityScores.toViewDU(stateGloasCloned.inactivityScores.getAll()); + stateGloasView.inactivityScores = migrateBasicListToGloas( + stateGloasCloned.inactivityScores, + ssz.gloas.InactivityScores + ); stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee; stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee; stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash; @@ -102,7 +118,7 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea /** * Migrate a composite list from fulu to its gloas progressive-list equivalent by reusing the fulu - * list's cached element nodes. + * list's element nodes. * * Works whenever the element type is identical across the fork (e.g. validators use ValidatorNodeStruct, * the pending* queues use the same electra element types). Each element's cached subtree root is then @@ -110,15 +126,38 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea * hashTreeRoot() skips re-hashing every element — the dominant cost for large lists like validators. * Much cheaper than `gloasType.toViewDU(fuluList.getAllReadonlyValues())`, which decodes every element * to a value and forces a full re-hash. + * + * The chunk nodes of a composite list ARE the element root nodes, so they are extracted directly + * with getNodesAtDepth instead of allocating a temporary ViewDU wrapper per element (getAllReadonly). + * Requires the fulu view to be committed (done at the top of upgradeStateToGloas). + */ +function migrateCompositeListToGloas< + ElementType extends CompositeType, CompositeView, CompositeViewDU>, +>(fuluList: ListCompositeTreeViewDU, gloasType: ProgressiveListCompositeType) { + const {length, type} = fuluList; + const elementNodes = getNodesAtDepth(fuluList.node.left, type.chunkDepth, 0, length); + return gloasType.getViewDU(progressiveListRootNode(elementNodes, length)); +} + +/** + * Migrate a basic list from fulu to its gloas progressive-list equivalent by reusing the fulu + * list's packed chunk leaf nodes. + * + * Packed leaf chunks are bit-identical between List[T, N] and ProgressiveList[T] (same 32-byte + * LE packing, zero-padded final chunk); only the superstructure above the leaves differs. Reusing + * the leaves avoids materializing the value array (getAll), re-serializing it, and allocating + * fresh LeafNodes — the gloas tree shares the leaf nodes with the fulu tree. + * Requires the fulu view to be committed (done at the top of upgradeStateToGloas). */ -function migrateCompositeListToGloas( - fuluList: {getAllReadonly(): {node: Node}[]}, - gloasType: {getViewDU(node: Node): V} -): V { - const elementNodes = fuluList.getAllReadonly().map((v) => v.node); - const chunksNode = progressiveSubtreeFillToContents(elementNodes); - const rootNode = new BranchNode(chunksNode, LeafNode.fromUint32(elementNodes.length)); - return gloasType.getViewDU(rootNode); +function migrateBasicListToGloas>( + fuluList: ListBasicTreeViewDU, + gloasType: ProgressiveListBasicType +) { + const {length, type} = fuluList; + const chunkCount = Math.ceil(length / type.itemsPerChunk); + // List root = BranchNode(chunksNode, lengthNode) → chunks tree is the left child + const chunkLeafNodes = getNodesAtDepth(fuluList.node.left, type.chunkDepth, 0, chunkCount); + return gloasType.getViewDU(progressiveListRootNode(chunkLeafNodes, length)); } /** diff --git a/packages/state-transition/src/util/index.ts b/packages/state-transition/src/util/index.ts index 3f619ce394c1..1056148c93fb 100644 --- a/packages/state-transition/src/util/index.ts +++ b/packages/state-transition/src/util/index.ts @@ -25,6 +25,7 @@ export * from "./shuffling.js"; export * from "./signatureSets.js"; export * from "./signingRoot.js"; export * from "./slot.js"; +export * from "./ssz.js"; export * from "./syncCommittee.js"; export * from "./validator.js"; export * from "./weakSubjectivity.js"; diff --git a/packages/state-transition/src/util/ssz.ts b/packages/state-transition/src/util/ssz.ts new file mode 100644 index 000000000000..73a0593256e7 --- /dev/null +++ b/packages/state-transition/src/util/ssz.ts @@ -0,0 +1,42 @@ +import {BranchNode, LeafNode, Node, zeroNode} from "@chainsafe/persistent-merkle-tree"; +import {progressiveSubtreeFillToContents} from "@chainsafe/ssz"; + +// TODO: move these utils to @chainsafe/ssz (progressive.ts, next to progressiveSubtreeFillToContents) + +/** Root node (chunks + length mix-in) of a zero-filled ProgressiveListBasicType of `length` items */ +export function zeroProgressiveListBasicRootNode(itemsPerChunk: number, length: number): Node { + const chunkCount = Math.ceil(length / itemsPerChunk); + // mirrors ssz's progressiveSubtreeCount (subtree capacities 1, 4, 16, ...) + let numSubtrees = 0; + for (let remaining = chunkCount, subtreeLength = 1; remaining > 0; subtreeLength *= 4) { + remaining -= Math.min(remaining, subtreeLength); + numSubtrees++; + } + return new BranchNode(zeroProgressiveNode(numSubtrees), LeafNode.fromUint32(length)); +} + +/** + * Root node of a progressive list from its chunk/element nodes + length mix-in. + * `nodes` are packed 32-byte chunk leaves for basic lists, or element root nodes for composite lists. + */ +export function progressiveListRootNode(nodes: Node[], length: number): Node { + return new BranchNode(progressiveSubtreeFillToContents(nodes), LeafNode.fromUint32(length)); +} + +/** + * Return the chunks node of a zero-filled progressive merkle list spanning `numSubtrees` + * balanced subtrees (chunk capacities 1, 4, 16, ... = 4^i; depths 0, 2, 4, ... = 2i). + * + * Not memoized: the zero data is already fully shared via the cached zeroNode(2i) subtrees; + * only the O(numSubtrees) spine BranchNodes (~10 at 2M validators) are allocated per call. + */ +function zeroProgressiveNode(numSubtrees: number): Node { + // chain(k) = B(zeroNode(0), B(zeroNode(2), ... B(zeroNode(2(k-1)), zeroNode(0)))) + let node: Node = zeroNode(0); // terminator + for (let i = numSubtrees - 1; i >= 0; i--) { + // BranchNode(left: balanced subtree i, right: rest of chain) — chain grows to the right, + // same as ssz progressiveSubtreeFillToContents: `root = new BranchNode(subtreeRoots[i], root)` + node = new BranchNode(zeroNode(2 * i), node); + } + return node; +} diff --git a/packages/state-transition/test/unit/epoch/processParticipationFlagUpdates.test.ts b/packages/state-transition/test/unit/epoch/processParticipationFlagUpdates.test.ts index bb449d44c6ff..7aed3a205b45 100644 --- a/packages/state-transition/test/unit/epoch/processParticipationFlagUpdates.test.ts +++ b/packages/state-transition/test/unit/epoch/processParticipationFlagUpdates.test.ts @@ -22,4 +22,23 @@ describe("processParticipationFlagUpdates", () => { Array.from({length: validatorCount}, () => 0) ); }); + + it("Gloas zeroed participation matches naive rebuild (root + bytes)", () => { + // Length crossing progressive subtree boundaries and not a multiple of itemsPerChunk (32) + const validatorCount = 673; + const state = ssz.gloas.BeaconState.defaultViewDU(); + state.previousEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( + Array.from({length: validatorCount}, () => 1) + ); + state.currentEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( + Array.from({length: validatorCount}, (_, i) => i % 8) + ); + + processParticipationFlagUpdates(state as unknown as CachedBeaconStateAltair); + + // Differential oracle: the naive implementation this fast path replaces + const naive = ssz.gloas.EpochParticipation.toViewDU(new Array(validatorCount).fill(0)); + expect(state.currentEpochParticipation.hashTreeRoot()).toEqual(naive.hashTreeRoot()); + expect(state.currentEpochParticipation.serialize()).toEqual(naive.serialize()); + }); }); diff --git a/packages/state-transition/test/unit/upgradeState.test.ts b/packages/state-transition/test/unit/upgradeState.test.ts index 13ca9abb0b4b..6f077bc2ca98 100644 --- a/packages/state-transition/test/unit/upgradeState.test.ts +++ b/packages/state-transition/test/unit/upgradeState.test.ts @@ -42,7 +42,9 @@ describe("upgradeState", () => { it("upgradeStateToGloas reuses composite-list nodes with identical merkle roots", () => { // Enough validators to span multiple progressive subtrees (capacities 1, 4, 16, 64, ...) and to // populate every slot's committee for the gloas PTC window computed during the upgrade. - const numValidators = 128; + // Not a multiple of itemsPerChunk (4 for uint64, 32 for uint8) so basic-list migration covers + // a zero-padded partial final chunk. + const numValidators = 130; const fuluStateView = ssz.fulu.BeaconState.defaultViewDU(); for (let i = 0; i < numValidators; i++) { const validator = ssz.phase0.Validator.defaultValue(); @@ -56,10 +58,11 @@ describe("upgradeState", () => { validator.exitEpoch = FAR_FUTURE_EPOCH; validator.withdrawableEpoch = FAR_FUTURE_EPOCH; fuluStateView.validators.push(ssz.phase0.Validator.toViewDU(validator)); - fuluStateView.balances.push(32e9); - fuluStateView.previousEpochParticipation.push(0); - fuluStateView.currentEpochParticipation.push(0); - fuluStateView.inactivityScores.push(0); + // Distinct per-index values so basic-list leaf reuse in the wrong order would be caught + fuluStateView.balances.push(32e9 + i); + fuluStateView.previousEpochParticipation.push(i % 8); + fuluStateView.currentEpochParticipation.push((i + 3) % 8); + fuluStateView.inactivityScores.push(i % 5); } // Populate the pending* composite queues so the node-reuse path is exercised for them too. @@ -101,6 +104,14 @@ describe("upgradeState", () => { const expectedPendingConsolidationsRoot = ssz.gloas.PendingConsolidations.hashTreeRoot( fuluState.pendingConsolidations.getAllReadonlyValues() ); + const expectedBalancesRoot = ssz.gloas.Balances.hashTreeRoot(fuluState.balances.getAll()); + const expectedPreviousEpochParticipationRoot = ssz.gloas.EpochParticipation.hashTreeRoot( + fuluState.previousEpochParticipation.getAll() + ); + const expectedCurrentEpochParticipationRoot = ssz.gloas.EpochParticipation.hashTreeRoot( + fuluState.currentEpochParticipation.getAll() + ); + const expectedInactivityScoresRoot = ssz.gloas.InactivityScores.hashTreeRoot(fuluState.inactivityScores.getAll()); const gloasState = upgradeStateToGloas(fuluState); @@ -110,6 +121,12 @@ describe("upgradeState", () => { expect(gloasState.pendingDeposits.hashTreeRoot()).toEqual(expectedPendingDepositsRoot); expect(gloasState.pendingPartialWithdrawals.hashTreeRoot()).toEqual(expectedPendingPartialWithdrawalsRoot); expect(gloasState.pendingConsolidations.hashTreeRoot()).toEqual(expectedPendingConsolidationsRoot); + // Basic-list leaf reuse must produce byte-identical merkle roots too + expect(gloasState.balances.hashTreeRoot()).toEqual(expectedBalancesRoot); + expect(gloasState.balances.getAll()).toEqual(fuluStateView.balances.getAll()); + expect(gloasState.previousEpochParticipation.hashTreeRoot()).toEqual(expectedPreviousEpochParticipationRoot); + expect(gloasState.currentEpochParticipation.hashTreeRoot()).toEqual(expectedCurrentEpochParticipationRoot); + expect(gloasState.inactivityScores.hashTreeRoot()).toEqual(expectedInactivityScoresRoot); // Full state still merkleizes and round-trips expect(() => gloasState.hashTreeRoot()).not.toThrow(); expect(() => gloasState.toValue()).not.toThrow(); diff --git a/packages/state-transition/test/unit/util/ssz.test.ts b/packages/state-transition/test/unit/util/ssz.test.ts new file mode 100644 index 000000000000..3998f893c0fd --- /dev/null +++ b/packages/state-transition/test/unit/util/ssz.test.ts @@ -0,0 +1,59 @@ +import {describe, expect, it} from "vitest"; +import {ssz} from "@lodestar/types"; +import {zeroProgressiveListBasicRootNode} from "../../../src/util/ssz.js"; + +describe("zeroProgressiveListBasicRootNode", () => { + // EpochParticipation is ProgressiveList[ParticipationFlags] (uint8) → 32 items per chunk. + // Progressive subtree chunk capacities are 1, 4, 16, 64, ... so cumulative chunk counts are + // 1, 5, 21, 85, ... = 32, 160, 672, 2720 items. Test lengths around every boundary. + const lengths = [0, 1, 31, 32, 33, 160, 161, 671, 672, 673, 2720]; + + for (const length of lengths) { + it(`equals naive zero-filled list length=${length}`, () => { + const fastView = ssz.gloas.EpochParticipation.getViewDU( + zeroProgressiveListBasicRootNode(ssz.gloas.EpochParticipation.itemsPerChunk, length) + ); + const naiveView = ssz.gloas.EpochParticipation.toViewDU(new Array(length).fill(0)); + + expect(fastView.length).toBe(length); + expect(fastView.hashTreeRoot()).toEqual(naiveView.hashTreeRoot()); + expect(fastView.serialize()).toEqual(naiveView.serialize()); + }); + } + + it("produces a mutable view backed by shared zero nodes", () => { + const length = 673; + const fastView = ssz.gloas.EpochParticipation.getViewDU( + zeroProgressiveListBasicRootNode(ssz.gloas.EpochParticipation.itemsPerChunk, length) + ); + const naiveView = ssz.gloas.EpochParticipation.toViewDU(new Array(length).fill(0)); + + for (const view of [fastView, naiveView]) { + view.set(0, 7); + view.set(Math.floor(length / 2), 3); + view.set(length - 1, 1); + view.push(5); + view.commit(); + } + + expect(fastView.getAll()).toEqual(naiveView.getAll()); + expect(fastView.hashTreeRoot()).toEqual(naiveView.hashTreeRoot()); + }); + + it("does not mutate shared zero nodes across views", () => { + const length = 100; + const viewA = ssz.gloas.EpochParticipation.getViewDU( + zeroProgressiveListBasicRootNode(ssz.gloas.EpochParticipation.itemsPerChunk, length) + ); + const viewB = ssz.gloas.EpochParticipation.getViewDU( + zeroProgressiveListBasicRootNode(ssz.gloas.EpochParticipation.itemsPerChunk, length) + ); + + viewA.set(50, 7); + viewA.commit(); + + expect(viewA.get(50)).toBe(7); + expect(viewB.get(50)).toBe(0); + expect(viewB.getAll()).toEqual(new Array(length).fill(0)); + }); +}); From 7f81f069a7dc8bb8613b233d8ac1240bf4e58f26 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 17 Jul 2026 15:14:29 +0700 Subject: [PATCH 2/3] fix: do not check deposits length to be compliant with gloas spec --- .../state-transition/src/block/processParentExecutionPayload.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index e863a4bd3f3b..dc00b09700fa 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -3,7 +3,6 @@ import { MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, - MAX_DEPOSIT_REQUESTS_PER_PAYLOAD, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT, @@ -128,7 +127,6 @@ function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: numbe } function assertExecutionRequestsWithinLimits(requests: gloas.ExecutionRequests): void { - assertMaxLength("deposits", requests.deposits.length, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD); assertMaxLength("withdrawals", requests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD); assertMaxLength("consolidations", requests.consolidations.length, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD); // New in GLOAS:EIP8282 From 69fb4ff1fbfddda615281c3f66227f4ac5e7e532 Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Fri, 17 Jul 2026 15:57:44 +0700 Subject: [PATCH 3/3] chore: more unit tests --- packages/state-transition/test/unit/upgradeState.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/state-transition/test/unit/upgradeState.test.ts b/packages/state-transition/test/unit/upgradeState.test.ts index 6f077bc2ca98..f0f4a0a35087 100644 --- a/packages/state-transition/test/unit/upgradeState.test.ts +++ b/packages/state-transition/test/unit/upgradeState.test.ts @@ -67,6 +67,8 @@ describe("upgradeState", () => { // Populate the pending* composite queues so the node-reuse path is exercised for them too. // Non-builder withdrawal credentials (default zeros) keep the deposits pending in-order. + // pendingConsolidations is deliberately left EMPTY to cover migrating an empty list + // (a realistic fork-boundary state) through the same node-reuse path. for (let i = 0; i < 5; i++) { const pendingDeposit = ssz.electra.PendingDeposit.defaultValue(); pendingDeposit.amount = 1000 + i; @@ -76,9 +78,6 @@ describe("upgradeState", () => { fuluStateView.pendingPartialWithdrawals.push( ssz.electra.PendingPartialWithdrawal.toViewDU(pendingPartialWithdrawal) ); - const pendingConsolidation = ssz.electra.PendingConsolidation.defaultValue(); - pendingConsolidation.sourceIndex = i; - fuluStateView.pendingConsolidations.push(ssz.electra.PendingConsolidation.toViewDU(pendingConsolidation)); } fuluStateView.commit(); @@ -121,6 +120,7 @@ describe("upgradeState", () => { expect(gloasState.pendingDeposits.hashTreeRoot()).toEqual(expectedPendingDepositsRoot); expect(gloasState.pendingPartialWithdrawals.hashTreeRoot()).toEqual(expectedPendingPartialWithdrawalsRoot); expect(gloasState.pendingConsolidations.hashTreeRoot()).toEqual(expectedPendingConsolidationsRoot); + expect(gloasState.pendingConsolidations.length).toBe(0); // Basic-list leaf reuse must produce byte-identical merkle roots too expect(gloasState.balances.hashTreeRoot()).toEqual(expectedBalancesRoot); expect(gloasState.balances.getAll()).toEqual(fuluStateView.balances.getAll());