@lodestar/state-transition 1.41.0-dev.702f7932c2 → 1.41.0-dev.988363dff3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/lib/block/isValidIndexedPayloadAttestation.js +1 -1
  2. package/lib/block/isValidIndexedPayloadAttestation.js.map +1 -1
  3. package/lib/block/processDepositRequest.d.ts +1 -1
  4. package/lib/block/processDepositRequest.d.ts.map +1 -1
  5. package/lib/block/processDepositRequest.js +6 -5
  6. package/lib/block/processDepositRequest.js.map +1 -1
  7. package/lib/block/processExecutionPayloadBid.d.ts.map +1 -1
  8. package/lib/block/processExecutionPayloadBid.js +5 -0
  9. package/lib/block/processExecutionPayloadBid.js.map +1 -1
  10. package/lib/block/processExecutionPayloadEnvelope.js +0 -9
  11. package/lib/block/processExecutionPayloadEnvelope.js.map +1 -1
  12. package/lib/block/processWithdrawals.d.ts.map +1 -1
  13. package/lib/block/processWithdrawals.js +9 -1
  14. package/lib/block/processWithdrawals.js.map +1 -1
  15. package/lib/cache/epochCache.d.ts +3 -3
  16. package/lib/cache/epochCache.d.ts.map +1 -1
  17. package/lib/cache/epochCache.js +13 -13
  18. package/lib/cache/epochCache.js.map +1 -1
  19. package/lib/signatureSets/indexedPayloadAttestation.d.ts +3 -4
  20. package/lib/signatureSets/indexedPayloadAttestation.d.ts.map +1 -1
  21. package/lib/signatureSets/indexedPayloadAttestation.js +4 -4
  22. package/lib/signatureSets/indexedPayloadAttestation.js.map +1 -1
  23. package/lib/slot/upgradeStateToGloas.d.ts.map +1 -1
  24. package/lib/slot/upgradeStateToGloas.js +50 -0
  25. package/lib/slot/upgradeStateToGloas.js.map +1 -1
  26. package/lib/util/gloas.d.ts.map +1 -1
  27. package/lib/util/gloas.js +10 -3
  28. package/lib/util/gloas.js.map +1 -1
  29. package/lib/util/seed.d.ts +20 -4
  30. package/lib/util/seed.d.ts.map +1 -1
  31. package/lib/util/seed.js +80 -8
  32. package/lib/util/seed.js.map +1 -1
  33. package/package.json +7 -7
  34. package/src/block/isValidIndexedPayloadAttestation.ts +1 -1
  35. package/src/block/processDepositRequest.ts +8 -5
  36. package/src/block/processExecutionPayloadBid.ts +8 -0
  37. package/src/block/processExecutionPayloadEnvelope.ts +0 -15
  38. package/src/block/processWithdrawals.ts +10 -1
  39. package/src/cache/epochCache.ts +24 -24
  40. package/src/signatureSets/indexedPayloadAttestation.ts +4 -6
  41. package/src/slot/upgradeStateToGloas.ts +74 -0
  42. package/src/util/gloas.ts +11 -3
  43. package/src/util/seed.ts +106 -18
@@ -36,6 +36,7 @@ import {
36
36
  import {
37
37
  computeActivationExitEpoch,
38
38
  computeEpochAtSlot,
39
+ computePayloadTimelinessCommitteesForEpoch,
39
40
  computeProposers,
40
41
  computeSyncPeriodAtEpoch,
41
42
  getActivationChurnLimit,
@@ -43,7 +44,6 @@ import {
43
44
  getSeed,
44
45
  isActiveValidator,
45
46
  isAggregatorFromCommitteeLength,
46
- naiveGetPayloadTimlinessCommitteeIndices,
47
47
  } from "../util/index.js";
48
48
  import {
49
49
  AttesterDuty,
@@ -64,7 +64,7 @@ import {
64
64
  computeSyncCommitteeCache,
65
65
  getSyncCommitteeCache,
66
66
  } from "./syncCommitteeCache.js";
67
- import {BeaconStateAllForks, BeaconStateAltair, BeaconStateGloas, ShufflingGetter} from "./types.js";
67
+ import {BeaconStateAllForks, BeaconStateAltair, ShufflingGetter} from "./types.js";
68
68
 
69
69
  /** `= PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT)` */
70
70
  export const PROPOSER_WEIGHT_FACTOR = PROPOSER_WEIGHT / (WEIGHT_DENOMINATOR - PROPOSER_WEIGHT);
@@ -235,8 +235,8 @@ export class EpochCache {
235
235
  nextSyncCommitteeIndexed: SyncCommitteeCache;
236
236
 
237
237
  // TODO GLOAS: See if we need to cached PTC for prev/next epoch
238
- // PTC for current epoch
239
- payloadTimelinessCommittee: ValidatorIndex[][];
238
+ // PTC for current epoch, computed eagerly at epoch transition
239
+ payloadTimelinessCommittees: Uint32Array[];
240
240
 
241
241
  // TODO: Helper stats
242
242
  syncPeriod: SyncPeriod;
@@ -275,7 +275,7 @@ export class EpochCache {
275
275
  previousTargetUnslashedBalanceIncrements: number;
276
276
  currentSyncCommitteeIndexed: SyncCommitteeCache;
277
277
  nextSyncCommitteeIndexed: SyncCommitteeCache;
278
- payloadTimelinessCommittee: ValidatorIndex[][];
278
+ payloadTimelinessCommittees: Uint32Array[];
279
279
  epoch: Epoch;
280
280
  syncPeriod: SyncPeriod;
281
281
  }) {
@@ -306,7 +306,7 @@ export class EpochCache {
306
306
  this.previousTargetUnslashedBalanceIncrements = data.previousTargetUnslashedBalanceIncrements;
307
307
  this.currentSyncCommitteeIndexed = data.currentSyncCommitteeIndexed;
308
308
  this.nextSyncCommitteeIndexed = data.nextSyncCommitteeIndexed;
309
- this.payloadTimelinessCommittee = data.payloadTimelinessCommittee;
309
+ this.payloadTimelinessCommittees = data.payloadTimelinessCommittees;
310
310
  this.epoch = data.epoch;
311
311
  this.syncPeriod = data.syncPeriod;
312
312
  }
@@ -457,14 +457,14 @@ export class EpochCache {
457
457
  nextSyncCommitteeIndexed = new SyncCommitteeCacheEmpty();
458
458
  }
459
459
 
460
- // Compute PTC for this epoch
461
- let payloadTimelinessCommittee: ValidatorIndex[][] = [];
460
+ // Compute PTC eagerly for all slots in the epoch
461
+ let payloadTimelinessCommittees: Uint32Array[] = [];
462
462
  if (currentEpoch >= config.GLOAS_FORK_EPOCH) {
463
- payloadTimelinessCommittee = naiveGetPayloadTimlinessCommitteeIndices(
464
- state as BeaconStateGloas,
465
- currentShuffling,
466
- effectiveBalanceIncrements,
467
- currentEpoch
463
+ payloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch(
464
+ state,
465
+ currentEpoch,
466
+ currentShuffling.committees,
467
+ effectiveBalanceIncrements
468
468
  );
469
469
  }
470
470
 
@@ -541,7 +541,7 @@ export class EpochCache {
541
541
  currentTargetUnslashedBalanceIncrements,
542
542
  currentSyncCommitteeIndexed,
543
543
  nextSyncCommitteeIndexed,
544
- payloadTimelinessCommittee: payloadTimelinessCommittee,
544
+ payloadTimelinessCommittees,
545
545
  epoch: currentEpoch,
546
546
  syncPeriod: computeSyncPeriodAtEpoch(currentEpoch),
547
547
  });
@@ -587,7 +587,7 @@ export class EpochCache {
587
587
  currentTargetUnslashedBalanceIncrements: this.currentTargetUnslashedBalanceIncrements,
588
588
  currentSyncCommitteeIndexed: this.currentSyncCommitteeIndexed,
589
589
  nextSyncCommitteeIndexed: this.nextSyncCommitteeIndexed,
590
- payloadTimelinessCommittee: this.payloadTimelinessCommittee,
590
+ payloadTimelinessCommittees: this.payloadTimelinessCommittees,
591
591
  epoch: this.epoch,
592
592
  syncPeriod: this.syncPeriod,
593
593
  });
@@ -698,11 +698,11 @@ export class EpochCache {
698
698
 
699
699
  this.proposersPrevEpoch = this.proposers;
700
700
  if (upcomingEpoch >= this.config.GLOAS_FORK_EPOCH) {
701
- this.payloadTimelinessCommittee = naiveGetPayloadTimlinessCommitteeIndices(
702
- state as BeaconStateGloas,
703
- this.currentShuffling,
704
- this.effectiveBalanceIncrements,
705
- upcomingEpoch
701
+ this.payloadTimelinessCommittees = computePayloadTimelinessCommitteesForEpoch(
702
+ state,
703
+ upcomingEpoch,
704
+ this.currentShuffling.committees,
705
+ this.effectiveBalanceIncrements
706
706
  );
707
707
  }
708
708
  if (upcomingEpoch >= this.config.FULU_FORK_EPOCH) {
@@ -1022,18 +1022,18 @@ export class EpochCache {
1022
1022
  return this.epoch >= this.config.ELECTRA_FORK_EPOCH;
1023
1023
  }
1024
1024
 
1025
- getPayloadTimelinessCommittee(slot: Slot): ValidatorIndex[] {
1025
+ getPayloadTimelinessCommittee(slot: Slot): Uint32Array {
1026
1026
  const epoch = computeEpochAtSlot(slot);
1027
1027
 
1028
1028
  if (epoch < this.config.GLOAS_FORK_EPOCH) {
1029
1029
  throw new Error("Payload Timeliness Committee is not available before gloas fork");
1030
1030
  }
1031
1031
 
1032
- if (epoch === this.epoch) {
1033
- return this.payloadTimelinessCommittee[slot % SLOTS_PER_EPOCH];
1032
+ if (epoch !== this.epoch) {
1033
+ throw new Error(`Payload Timeliness Committee is not available for slot=${slot}`);
1034
1034
  }
1035
1035
 
1036
- throw new Error(`Payload Timeliness Committee is not available for slot=${slot}`);
1036
+ return this.payloadTimelinessCommittees[slot % SLOTS_PER_EPOCH];
1037
1037
  }
1038
1038
 
1039
1039
  getIndexedPayloadAttestation(
@@ -1,26 +1,24 @@
1
1
  import {BeaconConfig} from "@lodestar/config";
2
2
  import {DOMAIN_PTC_ATTESTER} from "@lodestar/params";
3
- import {Slot, gloas, ssz} from "@lodestar/types";
4
- import {CachedBeaconStateGloas} from "../types.js";
3
+ import {gloas, ssz} from "@lodestar/types";
5
4
  import {ISignatureSet, computeSigningRoot, createAggregateSignatureSetFromComponents} from "../util/index.js";
6
5
 
7
6
  export function getIndexedPayloadAttestationSignatureSet(
8
- state: CachedBeaconStateGloas,
7
+ config: BeaconConfig,
9
8
  indexedPayloadAttestation: gloas.IndexedPayloadAttestation
10
9
  ): ISignatureSet {
11
10
  return createAggregateSignatureSetFromComponents(
12
11
  indexedPayloadAttestation.attestingIndices,
13
- getPayloadAttestationDataSigningRoot(state.config, state.slot, indexedPayloadAttestation.data),
12
+ getPayloadAttestationDataSigningRoot(config, indexedPayloadAttestation.data),
14
13
  indexedPayloadAttestation.signature
15
14
  );
16
15
  }
17
16
 
18
17
  export function getPayloadAttestationDataSigningRoot(
19
18
  config: BeaconConfig,
20
- stateSlot: Slot,
21
19
  data: gloas.PayloadAttestationData
22
20
  ): Uint8Array {
23
- const domain = config.getDomain(stateSlot, DOMAIN_PTC_ATTESTER);
21
+ const domain = config.getDomain(data.slot, DOMAIN_PTC_ATTESTER);
24
22
 
25
23
  return computeSigningRoot(ssz.gloas.PayloadAttestationData, data, domain);
26
24
  }
@@ -1,7 +1,12 @@
1
1
  import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
2
2
  import {ssz} from "@lodestar/types";
3
+ import {toHex} from "@lodestar/utils";
4
+ import {isValidDepositSignature} from "../block/processDeposit.js";
5
+ import {applyDepositForBuilder} from "../block/processDepositRequest.js";
3
6
  import {getCachedBeaconState} from "../cache/stateCache.js";
4
7
  import {CachedBeaconStateFulu, CachedBeaconStateGloas} from "../types.js";
8
+ import {isBuilderWithdrawalCredential} from "../util/gloas.js";
9
+ import {isValidatorKnown} from "../util/index.js";
5
10
 
6
11
  /**
7
12
  * Upgrade a state from Fulu to Gloas.
@@ -64,6 +69,9 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
64
69
 
65
70
  const stateGloas = getCachedBeaconState(stateGloasView, stateFulu);
66
71
 
72
+ // Process pending builder deposits at the fork boundary
73
+ onboardBuildersFromPendingDeposits(stateGloas);
74
+
67
75
  stateGloas.commit();
68
76
  // Clear cache to ensure the cache of fulu fields is not used by new gloas fields
69
77
  // biome-ignore lint/complexity/useLiteralKeys: It is a protected attribute
@@ -71,3 +79,69 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea
71
79
 
72
80
  return stateGloas;
73
81
  }
82
+
83
+ /**
84
+ * Applies any pending deposits for builders to onboard builders during the fork transition
85
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.2/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits
86
+ */
87
+ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void {
88
+ // Track pubkeys of new validators to keep their deposits pending
89
+ const validatorPubkeys = new Set<string>();
90
+
91
+ // Track pubkeys of new builders added when applying deposits
92
+ const builderPubkeys = new Set<string>();
93
+
94
+ const remainingPendingDeposits = ssz.electra.PendingDeposits.defaultViewDU();
95
+ for (let i = 0; i < state.pendingDeposits.length; i++) {
96
+ const deposit = state.pendingDeposits.getReadonly(i);
97
+
98
+ const validatorIndex = state.epochCtx.getValidatorIndex(deposit.pubkey);
99
+ const pubkeyHex = toHex(deposit.pubkey);
100
+
101
+ // Deposits for existing validators stay in pending queue
102
+ if (isValidatorKnown(state, validatorIndex) || validatorPubkeys.has(pubkeyHex)) {
103
+ remainingPendingDeposits.push(deposit);
104
+ continue;
105
+ }
106
+
107
+ // If the pubkey is associated with a builder that was created in a previous iteration
108
+ // or it is a builder deposit, try to apply the deposit to the new/existing builder
109
+ const isExistingBuilder = builderPubkeys.has(pubkeyHex);
110
+ const hasBuilderCredentials = isBuilderWithdrawalCredential(deposit.withdrawalCredentials);
111
+ if (isExistingBuilder || hasBuilderCredentials) {
112
+ const buildersLenBefore = state.builders.length;
113
+ applyDepositForBuilder(
114
+ state,
115
+ deposit.pubkey,
116
+ deposit.withdrawalCredentials,
117
+ deposit.amount,
118
+ deposit.signature,
119
+ deposit.slot
120
+ );
121
+ // Track newly added builders for subsequent iterations
122
+ if (!isExistingBuilder && state.builders.length > buildersLenBefore) {
123
+ builderPubkeys.add(pubkeyHex);
124
+ }
125
+ continue;
126
+ }
127
+
128
+ // If there is a pending deposit for a new validator that has a valid signature, track the
129
+ // pubkey so that subsequent builder deposits for the same pubkey stay in pending (applied to
130
+ // the validator later) rather than creating a builder. Deposits with invalid signatures are
131
+ // dropped here since they would fail in apply_pending_deposit anyway.
132
+ if (
133
+ isValidDepositSignature(
134
+ state.config,
135
+ deposit.pubkey,
136
+ deposit.withdrawalCredentials,
137
+ deposit.amount,
138
+ deposit.signature
139
+ )
140
+ ) {
141
+ validatorPubkeys.add(pubkeyHex);
142
+ remainingPendingDeposits.push(deposit);
143
+ }
144
+ }
145
+
146
+ state.pendingDeposits = remainingPendingDeposits;
147
+ }
package/src/util/gloas.ts CHANGED
@@ -28,12 +28,18 @@ export function getBuilderPaymentQuorumThreshold(state: CachedBeaconStateGloas):
28
28
  return Math.floor(quorum / BUILDER_PAYMENT_THRESHOLD_DENOMINATOR);
29
29
  }
30
30
 
31
+ function hasBuilderIndexFlag(index: number): boolean {
32
+ // Equivalent to `(index & BUILDER_INDEX_FLAG) != 0`
33
+ return Math.floor(index / BUILDER_INDEX_FLAG) % 2 === 1;
34
+ }
35
+
31
36
  /**
32
37
  * Check if a validator index represents a builder (has the builder flag set).
33
38
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-is_builder_index
34
39
  */
35
40
  export function isBuilderIndex(validatorIndex: number): boolean {
36
- return (validatorIndex & BUILDER_INDEX_FLAG) !== 0;
41
+ // Note: Can't use bitwise AND (&) because BUILDER_INDEX_FLAG exceeds 32 bits in JS bitwise operations.
42
+ return hasBuilderIndexFlag(validatorIndex);
37
43
  }
38
44
 
39
45
  /**
@@ -41,7 +47,8 @@ export function isBuilderIndex(validatorIndex: number): boolean {
41
47
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-convert_builder_index_to_validator_index
42
48
  */
43
49
  export function convertBuilderIndexToValidatorIndex(builderIndex: BuilderIndex): ValidatorIndex {
44
- return builderIndex | BUILDER_INDEX_FLAG;
50
+ // Note: Can't use bitwise OR (|) because BUILDER_INDEX_FLAG exceeds 32 bits in JS bitwise operations.
51
+ return hasBuilderIndexFlag(builderIndex) ? builderIndex : builderIndex + BUILDER_INDEX_FLAG;
45
52
  }
46
53
 
47
54
  /**
@@ -49,7 +56,8 @@ export function convertBuilderIndexToValidatorIndex(builderIndex: BuilderIndex):
49
56
  * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.1/specs/gloas/beacon-chain.md#new-convert_validator_index_to_builder_index
50
57
  */
51
58
  export function convertValidatorIndexToBuilderIndex(validatorIndex: ValidatorIndex): BuilderIndex {
52
- return validatorIndex & ~BUILDER_INDEX_FLAG;
59
+ // Note: Can't use bitwise AND (&) because BUILDER_INDEX_FLAG exceeds 32 bits in JS bitwise operations.
60
+ return hasBuilderIndexFlag(validatorIndex) ? validatorIndex - BUILDER_INDEX_FLAG : validatorIndex;
53
61
  }
54
62
 
55
63
  /**
package/src/util/seed.ts CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  import {Bytes32, DomainType, Epoch, ValidatorIndex} from "@lodestar/types";
22
22
  import {assert, bytesToBigInt, bytesToInt, intToBytes} from "@lodestar/utils";
23
23
  import {EffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js";
24
- import {BeaconStateAllForks, BeaconStateGloas, CachedBeaconStateAllForks} from "../types.js";
24
+ import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js";
25
25
  import {computeEpochAtSlot, computeStartSlotAtEpoch} from "./epoch.js";
26
26
 
27
27
  /**
@@ -268,29 +268,117 @@ export function getNextSyncCommitteeIndices(
268
268
  );
269
269
  }
270
270
 
271
- export function naiveGetPayloadTimlinessCommitteeIndices(
272
- state: BeaconStateGloas,
273
- shuffling: {committees: Uint32Array[][]},
274
- effectiveBalanceIncrements: EffectiveBalanceIncrements,
275
- epoch: Epoch
276
- ): ValidatorIndex[][] {
271
+ /**
272
+ * Compute PTC for all slots in an epoch eagerly.
273
+ */
274
+ export function computePayloadTimelinessCommitteesForEpoch(
275
+ state: BeaconStateAllForks,
276
+ epoch: number,
277
+ committees: Uint32Array[][],
278
+ effectiveBalanceIncrements: EffectiveBalanceIncrements
279
+ ): Uint32Array[] {
277
280
  const epochSeed = getSeed(state, epoch, DOMAIN_PTC_ATTESTER);
278
- const startSlot = computeStartSlotAtEpoch(epoch);
279
- const committeeIndices = [];
281
+ const startSlot = epoch * SLOTS_PER_EPOCH;
282
+ const result: Uint32Array[] = new Array(SLOTS_PER_EPOCH);
283
+
284
+ // Pre-allocate slot seed buffer once, reuse across all slots
285
+ const slotSeedInput = new Uint8Array(epochSeed.length + 8);
286
+ slotSeedInput.set(epochSeed, 0);
287
+ const slotSeedView = new DataView(slotSeedInput.buffer, slotSeedInput.byteOffset, slotSeedInput.byteLength);
288
+
289
+ for (let i = 0; i < SLOTS_PER_EPOCH; i++) {
290
+ const slot = startSlot + i;
291
+ // Write slot as little-endian uint64 (fits in uint32 range)
292
+ slotSeedView.setUint32(epochSeed.length, slot, true);
293
+ slotSeedView.setUint32(epochSeed.length + 4, 0, true);
294
+ const slotSeed = digest(slotSeedInput);
295
+
296
+ result[i] = computePayloadTimelinessCommitteeForSlot(slotSeed, committees[i], effectiveBalanceIncrements);
297
+ }
298
+ return result;
299
+ }
280
300
 
281
- for (let slot = startSlot; slot < startSlot + SLOTS_PER_EPOCH; slot++) {
282
- const slotCommittees = shuffling.committees[slot % SLOTS_PER_EPOCH];
283
- const indices = naiveComputePayloadTimelinessCommitteeIndices(
284
- effectiveBalanceIncrements,
285
- slotCommittees.flatMap((c) => Array.from(c)),
286
- digest(Buffer.concat([epochSeed, intToBytes(slot, 8)]))
287
- );
288
- committeeIndices.push(indices);
301
+ /**
302
+ * Compute PTC for a single slot.
303
+ */
304
+ export function computePayloadTimelinessCommitteeForSlot(
305
+ slotSeed: Uint8Array,
306
+ slotCommittees: Uint32Array[],
307
+ effectiveBalanceIncrements: EffectiveBalanceIncrements
308
+ ): Uint32Array {
309
+ // Concatenate all committee Uint32Arrays for this slot
310
+ const totalLen = slotCommittees.reduce((sum, c) => sum + c.length, 0);
311
+ const allIndices = new Uint32Array(totalLen);
312
+ let offset = 0;
313
+ for (const c of slotCommittees) {
314
+ allIndices.set(c, offset);
315
+ offset += c.length;
316
+ }
317
+ return computePayloadTimelinessCommitteeIndices(effectiveBalanceIncrements, allIndices, slotSeed);
318
+ }
319
+
320
+ /**
321
+ * Optimized version of PTC indices computation.
322
+ * Avoids BigInt conversions and uses DataView for efficient byte reading.
323
+ */
324
+ export function computePayloadTimelinessCommitteeIndices(
325
+ effectiveBalanceIncrements: EffectiveBalanceIncrements,
326
+ indices: Uint32Array,
327
+ seed: Uint8Array
328
+ ): Uint32Array {
329
+ if (indices.length === 0) {
330
+ throw Error("Validator indices must not be empty");
289
331
  }
290
332
 
291
- return committeeIndices;
333
+ const result = new Uint32Array(PTC_SIZE);
334
+ let resultLen = 0;
335
+
336
+ const MAX_RANDOM_VALUE = 0xffff; // 2^16 - 1
337
+ const MAX_EFFECTIVE_BALANCE_INCREMENT = MAX_EFFECTIVE_BALANCE_ELECTRA / EFFECTIVE_BALANCE_INCREMENT;
338
+ const indicesLen = indices.length;
339
+
340
+ // Pre-allocate hash input buffer: seed + 8 bytes for block index
341
+ const hashInput = new Uint8Array(seed.length + 8);
342
+ hashInput.set(seed, 0);
343
+ const hashInputView = new DataView(hashInput.buffer, hashInput.byteOffset, hashInput.byteLength);
344
+ const seedLen = seed.length;
345
+
346
+ let i = 0;
347
+ let randomBytesView: DataView = new DataView(new ArrayBuffer(0));
348
+ let lastBlock = -1;
349
+
350
+ while (resultLen < PTC_SIZE) {
351
+ const candidateIndex = indices[i % indicesLen];
352
+
353
+ // Only recompute hash every 16 iterations
354
+ const block = i >>> 4; // Math.floor(i / 16)
355
+ if (block !== lastBlock) {
356
+ // Write block as little-endian uint64 (block always fits in uint32 range)
357
+ hashInputView.setUint32(seedLen, block, true);
358
+ hashInputView.setUint32(seedLen + 4, 0, true);
359
+ const randomBytes = digest(hashInput);
360
+ randomBytesView = new DataView(randomBytes.buffer, randomBytes.byteOffset, randomBytes.byteLength);
361
+ lastBlock = block;
362
+ }
363
+
364
+ const randomValue = randomBytesView.getUint16((i & 15) * 2, true);
365
+
366
+ const effectiveBalanceIncrement = effectiveBalanceIncrements[candidateIndex];
367
+ if (effectiveBalanceIncrement * MAX_RANDOM_VALUE >= MAX_EFFECTIVE_BALANCE_INCREMENT * randomValue) {
368
+ result[resultLen++] = candidateIndex;
369
+ }
370
+ i += 1;
371
+ }
372
+
373
+ return result;
292
374
  }
293
375
 
376
+ /**
377
+ * Naive version of PTC indices computation.
378
+ * Used to verify the optimized `computePayloadTimelinessCommitteeIndices`.
379
+ *
380
+ * SLOW CODE - 🐢
381
+ */
294
382
  export function naiveComputePayloadTimelinessCommitteeIndices(
295
383
  effectiveBalanceIncrements: EffectiveBalanceIncrements,
296
384
  indices: ArrayLike<ValidatorIndex>,