From 9a4fe763af8208413a7493b57892d87c5aa5bc3f Mon Sep 17 00:00:00 2001 From: Tuyen Nguyen Date: Tue, 10 Mar 2026 11:28:28 +0700 Subject: [PATCH] perf: optimize PTC committee computation to avoid per-slot allocations Co-Authored-By: Claude Sonnet 4.6 --- .../state-transition/src/cache/epochCache.ts | 6 ++-- .../src/util/epochShuffling.ts | 9 ++++++ packages/state-transition/src/util/seed.ts | 29 ++++--------------- .../test/unit/util/seed.test.ts | 6 ++-- 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/packages/state-transition/src/cache/epochCache.ts b/packages/state-transition/src/cache/epochCache.ts index d87c998d1e8a..3e545049fff3 100644 --- a/packages/state-transition/src/cache/epochCache.ts +++ b/packages/state-transition/src/cache/epochCache.ts @@ -458,7 +458,7 @@ export class EpochCache { payloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch( state, currentEpoch, - currentShuffling.committees, + currentShuffling, effectiveBalanceIncrements ); @@ -466,7 +466,7 @@ export class EpochCache { previousPayloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch( state, previousEpoch, - previousShuffling.committees, + previousShuffling, effectiveBalanceIncrements ); } @@ -707,7 +707,7 @@ export class EpochCache { this.payloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch( state, upcomingEpoch, - this.currentShuffling.committees, + this.currentShuffling, this.effectiveBalanceIncrements ); } diff --git a/packages/state-transition/src/util/epochShuffling.ts b/packages/state-transition/src/util/epochShuffling.ts index 24880ec83adf..b20163cbb125 100644 --- a/packages/state-transition/src/util/epochShuffling.ts +++ b/packages/state-transition/src/util/epochShuffling.ts @@ -62,6 +62,15 @@ export function computeCommitteeCount(activeValidatorCount: number): number { return Math.max(1, Math.min(MAX_COMMITTEES_PER_SLOT, committeesPerSlot)); } +export function getShufflingSlotSubarray(shuffling: Uint32Array, slotInEpoch: number): Uint32Array { + const n = shuffling.length; + const committeesPerSlot = computeCommitteeCount(n); + const committeeCount = committeesPerSlot * SLOTS_PER_EPOCH; + const start = Math.floor((n * slotInEpoch * committeesPerSlot) / committeeCount); + const end = Math.floor((n * (slotInEpoch + 1) * committeesPerSlot) / committeeCount); + return shuffling.subarray(start, end); +} + function buildCommitteesFromShuffling(shuffling: Uint32Array): Uint32Array[][] { const activeValidatorCount = shuffling.length; const committeesPerSlot = computeCommitteeCount(activeValidatorCount); diff --git a/packages/state-transition/src/util/seed.ts b/packages/state-transition/src/util/seed.ts index 978f3e41f557..7f9329d0fd35 100644 --- a/packages/state-transition/src/util/seed.ts +++ b/packages/state-transition/src/util/seed.ts @@ -23,6 +23,7 @@ import {assert, bytesToBigInt, bytesToInt, intToBytes} from "@lodestar/utils"; import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js"; import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js"; import {computeEpochAtSlot, computeStartSlotAtEpoch} from "./epoch.js"; +import {EpochShuffling, getShufflingSlotSubarray} from "./epochShuffling.js"; /** * Compute proposer indices for an epoch @@ -274,7 +275,7 @@ export function getNextSyncCommitteeIndices( export function computePayloadTimelinessCommitteesForEpoch( state: BeaconStateAllForks, epoch: number, - committees: Uint32Array[][], + epochShuffling: EpochShuffling, effectiveBalanceIncrements: EffectiveBalanceIncrements ): Uint32Array[] { const epochSeed = getSeed(state, epoch, DOMAIN_PTC_ATTESTER); @@ -293,35 +294,17 @@ export function computePayloadTimelinessCommitteesForEpoch( slotSeedView.setUint32(epochSeed.length + 4, 0, true); const slotSeed = digest(slotSeedInput); - result[i] = computePayloadTimelinessCommitteeForSlot(slotSeed, committees[i], effectiveBalanceIncrements); + const slotCommitteeIndices = getShufflingSlotSubarray(epochShuffling.shuffling, i); + result[i] = computePayloadTimelinessCommitteeForSlot(effectiveBalanceIncrements, slotCommitteeIndices, slotSeed); } return result; } /** * Compute PTC for a single slot. - */ -export function computePayloadTimelinessCommitteeForSlot( - slotSeed: Uint8Array, - slotCommittees: Uint32Array[], - effectiveBalanceIncrements: EffectiveBalanceIncrements -): Uint32Array { - // Concatenate all committee Uint32Arrays for this slot - const totalLen = slotCommittees.reduce((sum, c) => sum + c.length, 0); - const allIndices = new Uint32Array(totalLen); - let offset = 0; - for (const c of slotCommittees) { - allIndices.set(c, offset); - offset += c.length; - } - return computePayloadTimelinessCommitteeIndices(effectiveBalanceIncrements, allIndices, slotSeed); -} - -/** - * Optimized version of PTC indices computation. * Avoids BigInt conversions and uses DataView for efficient byte reading. */ -export function computePayloadTimelinessCommitteeIndices( +export function computePayloadTimelinessCommitteeForSlot( effectiveBalanceIncrements: EffectiveBalanceIncrements, indices: Uint32Array, seed: Uint8Array @@ -375,7 +358,7 @@ export function computePayloadTimelinessCommitteeIndices( /** * Naive version of PTC indices computation. - * Used to verify the optimized `computePayloadTimelinessCommitteeIndices`. + * Used to verify the optimized `computePayloadTimelinessCommitteeForSlot`. * * SLOW CODE - 🐢 */ diff --git a/packages/state-transition/test/unit/util/seed.test.ts b/packages/state-transition/test/unit/util/seed.test.ts index 701ed95c1077..3cbfe5787e32 100644 --- a/packages/state-transition/test/unit/util/seed.test.ts +++ b/packages/state-transition/test/unit/util/seed.test.ts @@ -4,7 +4,7 @@ import {toHexString} from "@chainsafe/ssz"; import {ForkSeq, GENESIS_EPOCH, GENESIS_SLOT, SLOTS_PER_EPOCH} from "@lodestar/params"; import {bytesToInt} from "@lodestar/utils"; import { - computePayloadTimelinessCommitteeIndices, + computePayloadTimelinessCommitteeForSlot, computeProposerIndex, computeShuffledIndex, getComputeShuffledIndexFn, @@ -94,7 +94,7 @@ describe("electra getNextSyncCommitteeIndices", () => { } }); -describe("computePayloadTimelinessCommitteeIndices", () => { +describe("computePayloadTimelinessCommitteeForSlot", () => { const seed = crypto.randomBytes(32); const vc = 1000; const indices = new Uint32Array(Array.from({length: vc}, (_, i) => i)); @@ -105,7 +105,7 @@ describe("computePayloadTimelinessCommitteeIndices", () => { it("should be the same to the naive version", () => { const expected = naiveComputePayloadTimelinessCommitteeIndices(effectiveBalanceIncrements, indices, seed); - const result = computePayloadTimelinessCommitteeIndices(effectiveBalanceIncrements, indices, seed); + const result = computePayloadTimelinessCommitteeForSlot(effectiveBalanceIncrements, indices, seed); expect(result).toEqual(new Uint32Array(expected)); }); });