From 5b2acdfa72998e84985437f97f804950130e0a62 Mon Sep 17 00:00:00 2001 From: Nazar Hussain Date: Sat, 15 Aug 2026 11:32:46 +0500 Subject: [PATCH] feat: use zig backed shuffling list --- packages/beacon-node/package.json | 1 - .../test/spec/presets/shuffling.test.ts | 4 +- packages/state-transition/package.json | 1 - .../src/util/epochShuffling.ts | 22 +---- packages/state-transition/src/util/seed.ts | 14 +-- .../test/perf/hashing.test.ts | 4 +- .../test/unit/util/shuffling.test.ts | 18 +++- pnpm-lock.yaml | 97 ------------------- 8 files changed, 30 insertions(+), 131 deletions(-) diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index 316469649bf2..bdb2df81fcc3 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -154,7 +154,6 @@ "xxhash-wasm": "1.0.2" }, "devDependencies": { - "@chainsafe/swap-or-not-shuffle": "^1.2.1", "@libp2p/interface-internal": "^3.1.12", "@libp2p/logger": "^6.2.13", "@libp2p/utils": "^7.4.0", diff --git a/packages/beacon-node/test/spec/presets/shuffling.test.ts b/packages/beacon-node/test/spec/presets/shuffling.test.ts index 0c241660bdc5..8d7107a6f7f1 100644 --- a/packages/beacon-node/test/spec/presets/shuffling.test.ts +++ b/packages/beacon-node/test/spec/presets/shuffling.test.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import {unshuffleList} from "@chainsafe/swap-or-not-shuffle"; +import bindings from "@chainsafe/lodestar-z"; import {ACTIVE_PRESET, SHUFFLE_ROUND_COUNT} from "@lodestar/params"; import {InputType} from "@lodestar/spec-test-util"; import {bnToNum, fromHex} from "@lodestar/utils"; @@ -7,6 +7,8 @@ import {ethereumConsensusSpecsTests} from "../specTestVersioning.js"; import {specTestIterator} from "../utils/specTestIterator.js"; import {RunnerType, TestRunnerFn} from "../utils/types.js"; +const {unshuffleList} = bindings.shuffle; + const shuffling: TestRunnerFn = () => { return { testFunction: (testcase) => { diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index c932ac02155b..3226f0433d9f 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -69,7 +69,6 @@ "@chainsafe/lodestar-z": "catalog:", "@chainsafe/persistent-merkle-tree": "^1.3.1", "@chainsafe/ssz": "^1.6.3", - "@chainsafe/swap-or-not-shuffle": "^1.2.1", "@lodestar/config": "workspace:^", "@lodestar/params": "workspace:^", "@lodestar/types": "workspace:^", diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index e8dc151faa14..cfe899d84fc8 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -1,4 +1,4 @@ -import {asyncUnshuffleList, unshuffleList} from "@chainsafe/swap-or-not-shuffle"; +import bindings from "@chainsafe/lodestar-z"; import {BeaconConfig} from "@lodestar/config"; import { DOMAIN_BEACON_ATTESTER, @@ -16,6 +16,8 @@ import {computeAnchorCheckpoint} from "./computeAnchorCheckpoint.js"; import {computeStartSlotAtEpoch} from "./epoch.js"; import {getSeed} from "./seed.js"; +const {unshuffleList} = bindings.shuffle; + /** * Readonly interface for EpochShuffling. */ @@ -105,24 +107,6 @@ export function computeEpochShuffling( }; } -export async function computeEpochShufflingAsync( - // TODO: (@matthewkeil) remove state/epoch and pass in seed to clean this up - state: BeaconStateAllForks, - activeIndices: Uint32Array, - epoch: Epoch -): Promise { - const seed = getSeed(state, epoch, DOMAIN_BEACON_ATTESTER); - const shuffling = await asyncUnshuffleList(activeIndices, seed, SHUFFLE_ROUND_COUNT); - const committees = buildCommitteesFromShuffling(shuffling); - return { - epoch, - activeIndices, - shuffling, - committees, - committeesPerSlot: committees[0].length, - }; -} - export function calculateDecisionRoot(state: BeaconStateAllForks, epoch: Epoch): RootHex { const pivotSlot = Math.max(GENESIS_SLOT, computeStartSlotAtEpoch(epoch - 1) - 1); return toRootHex(getBlockRootAtSlot(state, pivotSlot)); diff --git a/packages/state-transition/src/util/seed.ts b/packages/state-transition/src/util/seed.ts index be01506f6cd4..f92fef73efc8 100644 --- a/packages/state-transition/src/util/seed.ts +++ b/packages/state-transition/src/util/seed.ts @@ -1,8 +1,5 @@ import {digest} from "@chainsafe/as-sha256"; -import { - computeProposerIndex as nativeComputeProposerIndex, - computeSyncCommitteeIndices as nativeComputeSyncCommitteeIndices, -} from "@chainsafe/swap-or-not-shuffle"; +import bindings from "@chainsafe/lodestar-z"; import { DOMAIN_BEACON_PROPOSER, DOMAIN_PTC_ATTESTER, @@ -24,6 +21,11 @@ import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "./epoch.js"; +const { + computeProposerIndex: nativeComputeProposerIndex, + computeSyncCommitteeIndices: nativeComputeSyncCommitteeIndices, +} = bindings.shuffle; + /** * Compute proposer indices for an epoch */ @@ -117,7 +119,7 @@ export function computeProposerIndex( } let maxEffectiveBalance: number; - let randByteCount: number; + let randByteCount: 1 | 2; if (fork >= ForkSeq.electra) { maxEffectiveBalance = MAX_EFFECTIVE_BALANCE_ELECTRA; randByteCount = 2; @@ -244,7 +246,7 @@ export function getNextSyncCommitteeIndices( effectiveBalanceIncrements: EffectiveBalanceIncrements ): Uint32Array { let maxEffectiveBalance: number; - let randByteCount: number; + let randByteCount: 1 | 2; if (fork >= ForkSeq.electra) { maxEffectiveBalance = MAX_EFFECTIVE_BALANCE_ELECTRA; diff --git a/packages/state-transition/test/perf/hashing.test.ts b/packages/state-transition/test/perf/hashing.test.ts index ea5aa1a81f34..0c1ae871faca 100644 --- a/packages/state-transition/test/perf/hashing.test.ts +++ b/packages/state-transition/test/perf/hashing.test.ts @@ -1,9 +1,11 @@ import {beforeAll, bench, describe} from "@chainsafe/benchmark"; -import {unshuffleList} from "@chainsafe/swap-or-not-shuffle"; +import bindings from "@chainsafe/lodestar-z"; import {SHUFFLE_ROUND_COUNT} from "@lodestar/params"; import {ssz} from "@lodestar/types"; import {generatePerfTestCachedStatePhase0, numValidators} from "../../src/testUtils/util.js"; +const {unshuffleList} = bindings.shuffle; + // Test cost of hashing state after some modifications describe("BeaconState hashTreeRoot", () => { diff --git a/packages/state-transition/test/unit/util/shuffling.test.ts b/packages/state-transition/test/unit/util/shuffling.test.ts index 9f01deee1c03..43a3047bce63 100644 --- a/packages/state-transition/test/unit/util/shuffling.test.ts +++ b/packages/state-transition/test/unit/util/shuffling.test.ts @@ -1,11 +1,12 @@ import {describe, expect, it} from "vitest"; +import {SLOTS_PER_EPOCH} from "@lodestar/params"; import {ssz} from "@lodestar/types"; import {computeEpochAtSlot} from "../../../src/index.js"; import {generateState} from "../../../src/testUtils/state.js"; -import {computeEpochShuffling, computeEpochShufflingAsync} from "../../../src/util/epochShuffling.js"; +import {computeEpochShuffling} from "../../../src/util/epochShuffling.js"; describe("EpochShuffling", () => { - it("async and sync versions should be identical", async () => { + it("should shuffle active indices into a permutation split into committees", () => { const numberOfValidators = 1000; const activeIndices = Uint32Array.from(Array.from({length: numberOfValidators}, (_, i) => i)); const state = generateState(); @@ -24,9 +25,16 @@ describe("EpochShuffling", () => { ); const epoch = computeEpochAtSlot(state.slot); - const sync = computeEpochShuffling(state, activeIndices, epoch); - const async = await computeEpochShufflingAsync(state, activeIndices, epoch); + const shuffling = computeEpochShuffling(state, activeIndices, epoch); - expect(sync).toStrictEqual(async); + expect(shuffling.epoch).toBe(epoch); + expect(shuffling.activeIndices).toBe(activeIndices); + expect(Array.from(shuffling.shuffling).sort((a, b) => a - b)).toEqual(Array.from(activeIndices)); + expect(shuffling.committees.length).toBe(SLOTS_PER_EPOCH); + const committeeSize = shuffling.committees.flat().reduce((sum, c) => sum + c.length, 0); + expect(committeeSize).toBe(numberOfValidators); + + // deterministic for the same state and epoch + expect(computeEpochShuffling(state, activeIndices, epoch)).toStrictEqual(shuffling); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa330f9d0e3f..fe1a7761a89c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -342,9 +342,6 @@ importers: specifier: 1.0.2 version: 1.0.2 devDependencies: - '@chainsafe/swap-or-not-shuffle': - specifier: ^1.2.1 - version: 1.2.1 '@libp2p/interface-internal': specifier: ^3.1.12 version: 3.1.12 @@ -776,9 +773,6 @@ importers: '@chainsafe/ssz': specifier: ^1.6.3 version: 1.6.3 - '@chainsafe/swap-or-not-shuffle': - specifier: ^1.2.1 - version: 1.2.1 '@lodestar/config': specifier: workspace:^ version: link:../config @@ -1284,62 +1278,6 @@ packages: '@chainsafe/ssz@1.6.3': resolution: {integrity: sha512-1O6N3rc5Uu1nn82lUkYzdr7kskcQGHwSMa7voLq6uBzfp8LroS+pcypYtTH8o2Pb2Zx2k2MYlkDRMo4kx/ckJA==} - '@chainsafe/swap-or-not-shuffle-darwin-arm64@1.2.1': - resolution: {integrity: sha512-kTewLZe1KqMAJ1gHfagOxo0BI4kTcMOAkGJ7pRLFM5ZkL2P7sh3/4ixnjdbtMkO207vMZEZL3fxJSSs14kg9Kg==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [darwin] - - '@chainsafe/swap-or-not-shuffle-darwin-x64@1.2.1': - resolution: {integrity: sha512-B/f/peQqOLW5Tqib5CqanAlQgoeib75FNszzHwRBwHdRY5ThOcBbARvOtef39zU5lYjj4j3iWobpD7G70kQyUw==} - engines: {node: '>= 18'} - cpu: [x64] - os: [darwin] - - '@chainsafe/swap-or-not-shuffle-linux-arm64-gnu@1.2.1': - resolution: {integrity: sha512-sDpuUuo3rStvHMQgLxH1UkkUbo8rcjDI70Fq3xzbNMhfjlrP0+sJistlFJtDpWmKsFk/sgje3PzVU9G3KFXzXA==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@chainsafe/swap-or-not-shuffle-linux-arm64-musl@1.2.1': - resolution: {integrity: sha512-FFmpDF2dRhhOCT3WBYOoQW9wqhJMfx7iULskBe5cjowVQ15U1TQJ6tC+hPpouUMt3/pKz9tIS9dNWTUuo7kpQg==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@chainsafe/swap-or-not-shuffle-linux-x64-gnu@1.2.1': - resolution: {integrity: sha512-nGXEnFCqRmCS7ATV7QQ7b7aKHcyET9XEpJylq0sljnkuNRwoQXFUYPMYTn4TMI6g3vgZt/AVK9RdCxp68JKwGQ==} - engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@chainsafe/swap-or-not-shuffle-linux-x64-musl@1.2.1': - resolution: {integrity: sha512-VMQxE/EZjco4damGrJkkCQ2Y2WYgJcTR/ardTnSctM1/xxwUD1wQr7YW2TQsECfK3UZcW2W6gG7iqWfJgMdGSw==} - engines: {node: '>= 18'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@chainsafe/swap-or-not-shuffle-win32-arm64-msvc@1.2.1': - resolution: {integrity: sha512-qV+ps6KSoR8blc3gMse1BOBUGcCtptnMYoqTMwFCIx9LGaeSo3vKu5XBQDwHIfAW73bZ+PHb5YUqkRgtWBiEUw==} - engines: {node: '>= 18'} - cpu: [arm64] - os: [win32] - - '@chainsafe/swap-or-not-shuffle-win32-x64-msvc@1.2.1': - resolution: {integrity: sha512-hsDN4O6PPpF4rEA1tMngMJAOrWTGAu8TAqdEwgdP+U25o8UVlz8W2N9697AMGRnnArN2BgYS0zCeUR9kuZ2XEQ==} - engines: {node: '>= 18'} - cpu: [x64] - os: [win32] - - '@chainsafe/swap-or-not-shuffle@1.2.1': - resolution: {integrity: sha512-H8YdEoXXv2Hw17gDWGOJEya4LHlBbpChJP3jDQRfIk9hhwr0c/zbBemBRmjADowZhArL+ymkO+j5hGaYySjdpw==} - engines: {node: '>= 18'} - '@chainsafe/threads@1.11.3': resolution: {integrity: sha512-wTIHTOOJ3MMRFtnJJT6KJCuauyv8pgs79m5ipspyPjHdKM9HJnkeZcoo06G3qArx2xMvd6MqNj1TLfnh5iFvaQ==} @@ -6905,41 +6843,6 @@ snapshots: '@chainsafe/as-sha256': 1.2.5 '@chainsafe/persistent-merkle-tree': 1.3.1 - '@chainsafe/swap-or-not-shuffle-darwin-arm64@1.2.1': - optional: true - - '@chainsafe/swap-or-not-shuffle-darwin-x64@1.2.1': - optional: true - - '@chainsafe/swap-or-not-shuffle-linux-arm64-gnu@1.2.1': - optional: true - - '@chainsafe/swap-or-not-shuffle-linux-arm64-musl@1.2.1': - optional: true - - '@chainsafe/swap-or-not-shuffle-linux-x64-gnu@1.2.1': - optional: true - - '@chainsafe/swap-or-not-shuffle-linux-x64-musl@1.2.1': - optional: true - - '@chainsafe/swap-or-not-shuffle-win32-arm64-msvc@1.2.1': - optional: true - - '@chainsafe/swap-or-not-shuffle-win32-x64-msvc@1.2.1': - optional: true - - '@chainsafe/swap-or-not-shuffle@1.2.1': - optionalDependencies: - '@chainsafe/swap-or-not-shuffle-darwin-arm64': 1.2.1 - '@chainsafe/swap-or-not-shuffle-darwin-x64': 1.2.1 - '@chainsafe/swap-or-not-shuffle-linux-arm64-gnu': 1.2.1 - '@chainsafe/swap-or-not-shuffle-linux-arm64-musl': 1.2.1 - '@chainsafe/swap-or-not-shuffle-linux-x64-gnu': 1.2.1 - '@chainsafe/swap-or-not-shuffle-linux-x64-musl': 1.2.1 - '@chainsafe/swap-or-not-shuffle-win32-arm64-msvc': 1.2.1 - '@chainsafe/swap-or-not-shuffle-win32-x64-msvc': 1.2.1 - '@chainsafe/threads@1.11.3': dependencies: callsites: 3.1.0