@lodestar/state-transition 1.47.0 → 1.48.0-dev.042c96183c

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 (99) hide show
  1. package/lib/block/index.d.ts.map +1 -1
  2. package/lib/block/index.js +44 -13
  3. package/lib/block/index.js.map +1 -1
  4. package/lib/block/processExecutionPayloadBid.d.ts +2 -2
  5. package/lib/block/processExecutionPayloadBid.d.ts.map +1 -1
  6. package/lib/block/processExecutionPayloadBid.js +3 -2
  7. package/lib/block/processExecutionPayloadBid.js.map +1 -1
  8. package/lib/block/processOperations.d.ts.map +1 -1
  9. package/lib/block/processOperations.js +62 -33
  10. package/lib/block/processOperations.js.map +1 -1
  11. package/lib/block/processParentExecutionPayload.d.ts.map +1 -1
  12. package/lib/block/processParentExecutionPayload.js +2 -16
  13. package/lib/block/processParentExecutionPayload.js.map +1 -1
  14. package/lib/block/processPayloadAttestation.d.ts +1 -1
  15. package/lib/block/processPayloadAttestation.d.ts.map +1 -1
  16. package/lib/block/processPayloadAttestation.js +2 -2
  17. package/lib/block/processPayloadAttestation.js.map +1 -1
  18. package/lib/block/processWithdrawals.d.ts.map +1 -1
  19. package/lib/block/processWithdrawals.js +8 -2
  20. package/lib/block/processWithdrawals.js.map +1 -1
  21. package/lib/block/types.d.ts +30 -0
  22. package/lib/block/types.d.ts.map +1 -1
  23. package/lib/block/types.js +32 -0
  24. package/lib/block/types.js.map +1 -1
  25. package/lib/epoch/processInactivityUpdates.d.ts.map +1 -1
  26. package/lib/epoch/processInactivityUpdates.js +7 -1
  27. package/lib/epoch/processInactivityUpdates.js.map +1 -1
  28. package/lib/epoch/processPtcWindow.d.ts.map +1 -1
  29. package/lib/epoch/processPtcWindow.js +12 -9
  30. package/lib/epoch/processPtcWindow.js.map +1 -1
  31. package/lib/index.d.ts +2 -2
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.js +1 -1
  34. package/lib/index.js.map +1 -1
  35. package/lib/lightClient/spec/index.d.ts +1 -1
  36. package/lib/lightClient/spec/index.d.ts.map +1 -1
  37. package/lib/lightClient/spec/index.js +1 -1
  38. package/lib/lightClient/spec/index.js.map +1 -1
  39. package/lib/lightClient/spec/utils.d.ts +13 -3
  40. package/lib/lightClient/spec/utils.d.ts.map +1 -1
  41. package/lib/lightClient/spec/utils.js +108 -34
  42. package/lib/lightClient/spec/utils.js.map +1 -1
  43. package/lib/metrics.d.ts +7 -1
  44. package/lib/metrics.d.ts.map +1 -1
  45. package/lib/metrics.js +12 -1
  46. package/lib/metrics.js.map +1 -1
  47. package/lib/rewards/blockRewards.d.ts.map +1 -1
  48. package/lib/rewards/blockRewards.js +1 -3
  49. package/lib/rewards/blockRewards.js.map +1 -1
  50. package/lib/signatureSets/index.d.ts.map +1 -1
  51. package/lib/signatureSets/index.js +8 -0
  52. package/lib/signatureSets/index.js.map +1 -1
  53. package/lib/slot/upgradeStateToGloas.js +1 -1
  54. package/lib/slot/upgradeStateToGloas.js.map +1 -1
  55. package/lib/stateView/beaconStateView.d.ts +8 -2
  56. package/lib/stateView/beaconStateView.d.ts.map +1 -1
  57. package/lib/stateView/beaconStateView.js +18 -1
  58. package/lib/stateView/beaconStateView.js.map +1 -1
  59. package/lib/stateView/computeNewStateRoot.d.ts +6 -0
  60. package/lib/stateView/computeNewStateRoot.d.ts.map +1 -0
  61. package/lib/stateView/computeNewStateRoot.js +28 -0
  62. package/lib/stateView/computeNewStateRoot.js.map +1 -0
  63. package/lib/stateView/interface.d.ts +20 -3
  64. package/lib/stateView/interface.d.ts.map +1 -1
  65. package/lib/stateView/interface.js.map +1 -1
  66. package/lib/stateView/nativeBeaconStateView.d.ts +6 -2
  67. package/lib/stateView/nativeBeaconStateView.d.ts.map +1 -1
  68. package/lib/stateView/nativeBeaconStateView.js +15 -0
  69. package/lib/stateView/nativeBeaconStateView.js.map +1 -1
  70. package/lib/util/gloas.d.ts.map +1 -1
  71. package/lib/util/gloas.js +1 -1
  72. package/lib/util/gloas.js.map +1 -1
  73. package/lib/util/seed.d.ts +1 -1
  74. package/lib/util/seed.d.ts.map +1 -1
  75. package/lib/util/seed.js +14 -11
  76. package/lib/util/seed.js.map +1 -1
  77. package/package.json +8 -8
  78. package/src/block/index.ts +53 -17
  79. package/src/block/processExecutionPayloadBid.ts +6 -5
  80. package/src/block/processOperations.ts +63 -45
  81. package/src/block/processParentExecutionPayload.ts +2 -27
  82. package/src/block/processPayloadAttestation.ts +3 -2
  83. package/src/block/processWithdrawals.ts +12 -2
  84. package/src/block/types.ts +32 -0
  85. package/src/epoch/processInactivityUpdates.ts +8 -1
  86. package/src/epoch/processPtcWindow.ts +15 -8
  87. package/src/index.ts +3 -1
  88. package/src/lightClient/spec/index.ts +2 -0
  89. package/src/lightClient/spec/utils.ts +142 -48
  90. package/src/metrics.ts +13 -2
  91. package/src/rewards/blockRewards.ts +1 -3
  92. package/src/signatureSets/index.ts +17 -3
  93. package/src/slot/upgradeStateToGloas.ts +1 -1
  94. package/src/stateView/beaconStateView.ts +32 -1
  95. package/src/stateView/computeNewStateRoot.ts +35 -0
  96. package/src/stateView/interface.ts +27 -1
  97. package/src/stateView/nativeBeaconStateView.ts +23 -1
  98. package/src/util/gloas.ts +1 -0
  99. package/src/util/seed.ts +25 -13
@@ -23,18 +23,18 @@ import {
23
23
  } from "@lodestar/params";
24
24
  import {
25
25
  BeaconBlockHeader,
26
+ LightClientBootstrap,
26
27
  LightClientFinalityUpdate,
27
28
  LightClientHeader,
28
29
  LightClientOptimisticUpdate,
29
30
  LightClientUpdate,
30
- Slot,
31
31
  SyncCommittee,
32
32
  isElectraLightClientUpdate,
33
33
  ssz,
34
34
  } from "@lodestar/types";
35
- import {byteArrayEquals, verifyMerkleBranch} from "@lodestar/utils";
36
- import {computeEpochAtSlot, computeSyncPeriodAtSlot} from "../../util/epoch.js";
37
- import type {LightClientStore, SyncCommitteeFast} from "./store.js";
35
+ import {byteArrayEquals, hash, verifyMerkleBranch} from "@lodestar/utils";
36
+ import {computeEpochAtSlot} from "../../util/epoch.js";
37
+ import type {ILightClientStore, SyncCommitteeFast} from "./store.js";
38
38
 
39
39
  export const GENESIS_SLOT = 0;
40
40
  export const ZERO_HASH = new Uint8Array(32);
@@ -201,6 +201,76 @@ function getGindexIndex(gindex: number): number {
201
201
  return gindex - 2 ** getGindexDepth(gindex);
202
202
  }
203
203
 
204
+ /**
205
+ * Returns the execution root represented by a light-client header.
206
+ *
207
+ * Gloas headers removed `execution` so Pre-Gloas headers upgraded to the Gloas format
208
+ * require reconstruction of the original execution payload header root.
209
+ *
210
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/light-client/sync-protocol.md#modified-get_lc_execution_root
211
+ */
212
+ export function getLightClientExecutionRoot(config: ChainForkConfig, header: LightClientHeader): Uint8Array {
213
+ const epoch = computeEpochAtSlot(header.beacon.slot);
214
+
215
+ if (!isGloasLightClientHeader(header)) {
216
+ if (epoch < config.CAPELLA_FORK_EPOCH) {
217
+ return ZERO_HASH;
218
+ }
219
+
220
+ return config
221
+ .getPostBellatrixForkTypes(header.beacon.slot)
222
+ .ExecutionPayloadHeader.hashTreeRoot((header as LightClientHeader<ForkName.capella>).execution);
223
+ }
224
+
225
+ if (epoch >= config.GLOAS_FORK_EPOCH) {
226
+ return header.executionBlockHash;
227
+ }
228
+
229
+ // Reconstruct the pre-Gloas execution payload root for a header upgraded to the Gloas format
230
+ // because the structure is different, `execution` is removed
231
+ const executionPayloadDepth = getGindexDepth(BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX);
232
+ const innerBranch = header.executionBranch.slice(0, -executionPayloadDepth);
233
+
234
+ if (epoch >= config.DENEB_FORK_EPOCH) {
235
+ if (header.beacon.slot === GENESIS_SLOT) {
236
+ return ssz.deneb.ExecutionPayloadHeader.hashTreeRoot(ssz.deneb.ExecutionPayloadHeader.defaultValue());
237
+ }
238
+ return computeBranchRoot(
239
+ header.executionBlockHash,
240
+ innerBranch,
241
+ ssz.deneb.ExecutionPayloadHeader.getPathInfo(["blockHash"]).gindex
242
+ );
243
+ }
244
+
245
+ if (epoch >= config.CAPELLA_FORK_EPOCH) {
246
+ if (header.beacon.slot === GENESIS_SLOT) {
247
+ return ssz.capella.ExecutionPayloadHeader.hashTreeRoot(ssz.capella.ExecutionPayloadHeader.defaultValue());
248
+ }
249
+ return computeBranchRoot(
250
+ header.executionBlockHash,
251
+ innerBranch,
252
+ ssz.capella.ExecutionPayloadHeader.getPathInfo(["blockHash"]).gindex
253
+ );
254
+ }
255
+
256
+ return ZERO_HASH;
257
+ }
258
+
259
+ export function upgradeLightClientBootstrap(
260
+ config: ChainForkConfig,
261
+ targetFork: ForkName,
262
+ bootstrap: LightClientBootstrap
263
+ ): LightClientBootstrap {
264
+ return {
265
+ ...bootstrap,
266
+ header: upgradeLightClientHeader(config, targetFork, bootstrap.header),
267
+ currentSyncCommitteeBranch: normalizeMerkleBranch(
268
+ bootstrap.currentSyncCommitteeBranch,
269
+ currentSyncCommitteeGindexAtFork(targetFork)
270
+ ),
271
+ };
272
+ }
273
+
204
274
  export function upgradeLightClientHeader(
205
275
  config: ChainForkConfig,
206
276
  targetFork: ForkName,
@@ -211,9 +281,7 @@ export function upgradeLightClientHeader(
211
281
  throw Error(`Invalid upgrade request from headerFork=${headerFork} to targetFork=${targetFork}`);
212
282
  }
213
283
 
214
- // We are modifying the same header object, may be we could create a copy, but its
215
- // not required as of now
216
- let upgradedHeader = header;
284
+ let upgradedHeader = {...header} as LightClientHeader;
217
285
  const startUpgradeFromFork = Object.values(ForkName)[ForkSeq[headerFork] + 1];
218
286
 
219
287
  switch (startUpgradeFromFork) {
@@ -231,23 +299,30 @@ export function upgradeLightClientHeader(
231
299
 
232
300
  // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
233
301
  case ForkName.capella:
234
- (upgradedHeader as LightClientHeader<ForkName.capella>).execution =
235
- ssz.capella.LightClientHeader.fields.execution.defaultValue();
236
- (upgradedHeader as LightClientHeader<ForkName.capella>).executionBranch =
237
- ssz.capella.LightClientHeader.fields.executionBranch.defaultValue();
302
+ upgradedHeader = {
303
+ ...upgradedHeader,
304
+ execution: ssz.capella.LightClientHeader.fields.execution.defaultValue(),
305
+ executionBranch: ssz.capella.LightClientHeader.fields.executionBranch.defaultValue(),
306
+ } as LightClientHeader<ForkName.capella>;
238
307
 
239
308
  // Break if no further upgradation is required else fall through
240
309
  if (ForkSeq[targetFork] <= ForkSeq.capella) break;
241
310
 
242
311
  // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
243
- case ForkName.deneb:
244
- (upgradedHeader as LightClientHeader<ForkName.deneb>).execution.blobGasUsed =
245
- ssz.deneb.LightClientHeader.fields.execution.fields.blobGasUsed.defaultValue();
246
- (upgradedHeader as LightClientHeader<ForkName.deneb>).execution.excessBlobGas =
247
- ssz.deneb.LightClientHeader.fields.execution.fields.excessBlobGas.defaultValue();
312
+ case ForkName.deneb: {
313
+ const capellaHeader = upgradedHeader as LightClientHeader<ForkName.capella>;
314
+ upgradedHeader = {
315
+ ...capellaHeader,
316
+ execution: {
317
+ ...capellaHeader.execution,
318
+ blobGasUsed: ssz.deneb.LightClientHeader.fields.execution.fields.blobGasUsed.defaultValue(),
319
+ excessBlobGas: ssz.deneb.LightClientHeader.fields.execution.fields.excessBlobGas.defaultValue(),
320
+ },
321
+ } as LightClientHeader<ForkName.deneb>;
248
322
 
249
323
  // Break if no further upgradation is required else fall through
250
324
  if (ForkSeq[targetFork] <= ForkSeq.deneb) break;
325
+ }
251
326
 
252
327
  // biome-ignore lint/suspicious/noFallthroughSwitchClause: We need fall-through behavior here
253
328
  case ForkName.electra:
@@ -361,15 +436,16 @@ export function upgradeLightClientUpdate(
361
436
  targetFork: ForkName,
362
437
  update: LightClientUpdate
363
438
  ): LightClientUpdate {
364
- update.attestedHeader = upgradeLightClientHeader(config, targetFork, update.attestedHeader);
365
- update.finalizedHeader = upgradeLightClientHeader(config, targetFork, update.finalizedHeader);
366
- update.nextSyncCommitteeBranch = normalizeMerkleBranch(
367
- update.nextSyncCommitteeBranch,
368
- nextSyncCommitteeGindexAtFork(targetFork)
369
- );
370
- update.finalityBranch = normalizeMerkleBranch(update.finalityBranch, finalizedRootGindexAtFork(targetFork));
371
-
372
- return update;
439
+ return {
440
+ ...update,
441
+ attestedHeader: upgradeLightClientHeader(config, targetFork, update.attestedHeader),
442
+ finalizedHeader: upgradeLightClientHeader(config, targetFork, update.finalizedHeader),
443
+ nextSyncCommitteeBranch: normalizeMerkleBranch(
444
+ update.nextSyncCommitteeBranch,
445
+ nextSyncCommitteeGindexAtFork(targetFork)
446
+ ),
447
+ finalityBranch: normalizeMerkleBranch(update.finalityBranch, finalizedRootGindexAtFork(targetFork)),
448
+ };
373
449
  }
374
450
 
375
451
  export function upgradeLightClientFinalityUpdate(
@@ -377,14 +453,12 @@ export function upgradeLightClientFinalityUpdate(
377
453
  targetFork: ForkName,
378
454
  finalityUpdate: LightClientFinalityUpdate
379
455
  ): LightClientFinalityUpdate {
380
- finalityUpdate.attestedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.attestedHeader);
381
- finalityUpdate.finalizedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.finalizedHeader);
382
- finalityUpdate.finalityBranch = normalizeMerkleBranch(
383
- finalityUpdate.finalityBranch,
384
- finalizedRootGindexAtFork(targetFork)
385
- );
386
-
387
- return finalityUpdate;
456
+ return {
457
+ ...finalityUpdate,
458
+ attestedHeader: upgradeLightClientHeader(config, targetFork, finalityUpdate.attestedHeader),
459
+ finalizedHeader: upgradeLightClientHeader(config, targetFork, finalityUpdate.finalizedHeader),
460
+ finalityBranch: normalizeMerkleBranch(finalityUpdate.finalityBranch, finalizedRootGindexAtFork(targetFork)),
461
+ };
388
462
  }
389
463
 
390
464
  export function upgradeLightClientOptimisticUpdate(
@@ -392,9 +466,10 @@ export function upgradeLightClientOptimisticUpdate(
392
466
  targetFork: ForkName,
393
467
  optimisticUpdate: LightClientOptimisticUpdate
394
468
  ): LightClientOptimisticUpdate {
395
- optimisticUpdate.attestedHeader = upgradeLightClientHeader(config, targetFork, optimisticUpdate.attestedHeader);
396
-
397
- return optimisticUpdate;
469
+ return {
470
+ ...optimisticUpdate,
471
+ attestedHeader: upgradeLightClientHeader(config, targetFork, optimisticUpdate.attestedHeader),
472
+ };
398
473
  }
399
474
 
400
475
  /**
@@ -405,25 +480,44 @@ export function upgradeLightClientOptimisticUpdate(
405
480
  export function upgradeLightClientStore(
406
481
  config: ChainForkConfig,
407
482
  targetFork: ForkName,
408
- store: LightClientStore,
409
- signatureSlot: Slot
410
- ): LightClientStore {
411
- const updateSignaturePeriod = computeSyncPeriodAtSlot(signatureSlot);
412
- const bestValidUpdate = store.bestValidUpdates.get(updateSignaturePeriod);
413
-
414
- if (bestValidUpdate) {
415
- store.bestValidUpdates.set(updateSignaturePeriod, {
416
- update: upgradeLightClientUpdate(config, targetFork, bestValidUpdate.update),
417
- summary: bestValidUpdate.summary,
483
+ store: ILightClientStore
484
+ ): ILightClientStore {
485
+ const bestValidUpdates = new Map();
486
+
487
+ for (const [period, entry] of store.bestValidUpdates) {
488
+ bestValidUpdates.set(period, {
489
+ ...entry,
490
+ update: upgradeLightClientUpdate(config, targetFork, entry.update),
418
491
  });
419
492
  }
493
+ const finalizedHeader = upgradeLightClientHeader(config, targetFork, store.finalizedHeader);
494
+ const optimisticHeader = upgradeLightClientHeader(config, targetFork, store.optimisticHeader);
420
495
 
421
- store.finalizedHeader = upgradeLightClientHeader(config, targetFork, store.finalizedHeader);
422
- store.optimisticHeader = upgradeLightClientHeader(config, targetFork, store.optimisticHeader);
496
+ for (const [period, bestValidUpdate] of bestValidUpdates) {
497
+ store.bestValidUpdates.set(period, bestValidUpdate);
498
+ }
499
+ store.finalizedHeader = finalizedHeader;
500
+ store.optimisticHeader = optimisticHeader;
423
501
 
424
502
  return store;
425
503
  }
426
504
 
505
+ function computeBranchRoot(leaf: Uint8Array, branch: Uint8Array[], gindex: bigint): Uint8Array {
506
+ const depth = getGindexDepth(Number(gindex));
507
+ if (branch.length < depth) {
508
+ throw Error(`Invalid Merkle branch length ${branch.length}, expected at least ${depth}`);
509
+ }
510
+ const proof = branch.slice(-depth);
511
+ const index = getGindexIndex(Number(gindex));
512
+ let root = leaf;
513
+
514
+ for (let i = 0; i < depth; i++) {
515
+ root = Math.floor(index / 2 ** i) % 2 === 1 ? hash(proof[i], root) : hash(root, proof[i]);
516
+ }
517
+
518
+ return root;
519
+ }
520
+
427
521
  function isGloasLightClientHeader(header: LightClientHeader): header is LightClientHeader<ForkName.gloas> {
428
522
  return (header as LightClientHeader<ForkName.gloas>).executionBlockHash !== undefined;
429
523
  }
package/src/metrics.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {ForkName} from "@lodestar/params";
2
2
  import {MetricsRegister} from "@lodestar/utils";
3
- import {ProposerRewardType} from "./block/types.js";
3
+ import {BlockProcessStep, ProcessOperationsStep, ProposerRewardType} from "./block/types.js";
4
4
  import {EpochTransitionStep} from "./epoch/index.js";
5
5
  import {StateCloneSource, StateHashTreeRootSource} from "./stateTransition.js";
6
6
  import {CachedBeaconStateAllForks} from "./types.js";
@@ -57,10 +57,21 @@ export function getMetrics(register: MetricsRegister) {
57
57
  processBlockTime: register.histogram({
58
58
  name: "lodestar_stfn_process_block_seconds",
59
59
  help: "Time to process a single block in seconds",
60
- // TODO: Add metrics for each step
61
60
  // Block processing can take 5-40ms, 100ms max
62
61
  buckets: [0.005, 0.01, 0.02, 0.05, 0.1, 1],
63
62
  }),
63
+ processBlockStepTime: register.histogram<{step: BlockProcessStep}>({
64
+ name: "lodestar_stfn_process_block_step_seconds",
65
+ help: "Time to call each step of process block in seconds",
66
+ labelNames: ["step"],
67
+ buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1],
68
+ }),
69
+ processOperationsStepTime: register.histogram<{step: ProcessOperationsStep}>({
70
+ name: "lodestar_stfn_process_operations_step_seconds",
71
+ help: "Time to call each step of process operations in seconds",
72
+ labelNames: ["step"],
73
+ buckets: [0.001, 0.005, 0.01, 0.025, 0.05, 0.1],
74
+ }),
64
75
  processBlockCommitTime: register.histogram({
65
76
  name: "lodestar_stfn_process_block_commit_seconds",
66
77
  help: "Time to call commit after process a single block in seconds",
@@ -42,9 +42,7 @@ export async function computeBlockRewards(
42
42
  let syncAggregateReward = cachedSyncAggregateReward;
43
43
 
44
44
  if (blockAttestationReward === 0) {
45
- const parentSlot = isForkPostGloas(fork)
46
- ? (preState as CachedBeaconStateGloas).latestExecutionPayloadBid.slot
47
- : null;
45
+ const parentSlot = isForkPostGloas(fork) ? (preState as CachedBeaconStateGloas).latestBlockHeader.slot : null;
48
46
  blockAttestationReward =
49
47
  fork === ForkName.phase0
50
48
  ? computeBlockAttestationRewardPhase0(block as phase0.BeaconBlock, preState as CachedBeaconStatePhase0)
@@ -1,13 +1,14 @@
1
1
  import {BeaconConfig} from "@lodestar/config";
2
2
  import {ForkSeq} from "@lodestar/params";
3
- import {IndexedAttestation, SignedBeaconBlock, altair, capella} from "@lodestar/types";
3
+ import {IndexedAttestation, SignedBeaconBlock, altair, capella, gloas} from "@lodestar/types";
4
4
  import {getSyncCommitteeSignatureSet} from "../block/processSyncCommittee.js";
5
5
  import {SyncCommitteeCache} from "../cache/syncCommitteeCache.js";
6
- import {IBeaconStateView} from "../stateView/interface.js";
7
- import {ISignatureSet} from "../util/index.js";
6
+ import {IBeaconStateView, IBeaconStateViewGloas} from "../stateView/interface.js";
7
+ import {ISignatureSet, createAggregateSignatureSetFromComponents} from "../util/index.js";
8
8
  import {getAttesterSlashingsSignatureSets} from "./attesterSlashings.js";
9
9
  import {getBlsToExecutionChangeSignatureSets} from "./blsToExecutionChange.js";
10
10
  import {getAttestationsSignatureSets} from "./indexedAttestation.js";
11
+ import {getPayloadAttestationDataSigningRoot} from "./indexedPayloadAttestation.js";
11
12
  import {getBlockProposerSignatureSet} from "./proposer.js";
12
13
  import {getProposerSlashingsSignatureSets} from "./proposerSlashings.js";
13
14
  import {getRandaoRevealSignatureSet} from "./randao.js";
@@ -79,5 +80,18 @@ export function getBlockSignatureSets(
79
80
  }
80
81
  }
81
82
 
83
+ if (fork >= ForkSeq.gloas) {
84
+ for (const payloadAttestation of (signedBlock as gloas.SignedBeaconBlock).message.body.payloadAttestations) {
85
+ const ptc = (state as IBeaconStateViewGloas).getPayloadTimelinessCommittee(payloadAttestation.data.slot);
86
+ signatureSets.push(
87
+ createAggregateSignatureSetFromComponents(
88
+ payloadAttestation.aggregationBits.intersectValues(ptc),
89
+ getPayloadAttestationDataSigningRoot(config, payloadAttestation.data),
90
+ payloadAttestation.signature
91
+ )
92
+ );
93
+ }
94
+ }
95
+
82
96
  return signatureSets;
83
97
  }
@@ -110,7 +110,7 @@ export function upgradeStateToGloas(
110
110
  ssz.gloas.PendingConsolidations
111
111
  );
112
112
  stateGloasView.proposerLookahead = stateGloasCloned.proposerLookahead;
113
- stateGloasView.ptcWindow = ssz.gloas.PtcWindow.toViewDU(initializePtcWindow(stateFulu));
113
+ stateGloasView.ptcWindow = ssz.gloas.PayloadTimelinessCommitteeWindow.toViewDU(initializePtcWindow(stateFulu));
114
114
 
115
115
  for (let i = 0; i < SLOTS_PER_HISTORICAL_ROOT; i++) {
116
116
  stateGloasView.executionPayloadAvailability.set(i, true);
@@ -70,7 +70,15 @@ import {loadState} from "../util/loadState/loadState.js";
70
70
  import {PreVerifyBuilderDepositsResult, preVerifyBuilderDepositsPreGloas} from "../util/preVerifyBuilderDeposits.js";
71
71
  import {getRandaoMix} from "../util/seed.js";
72
72
  import {getLatestWeakSubjectivityCheckpointEpoch} from "../util/weakSubjectivity.js";
73
- import {IBeaconStateView, IBeaconStateViewGloas, IBeaconStateViewLatestFork, isStatePostGloas} from "./interface.js";
73
+ import {computeNewStateRootStateTransitionOpts, getComputeNewStateRootResult} from "./computeNewStateRoot.js";
74
+ import {
75
+ ComputeNewStateRootInput,
76
+ ComputeNewStateRootResult,
77
+ IBeaconStateView,
78
+ IBeaconStateViewGloas,
79
+ IBeaconStateViewLatestFork,
80
+ isStatePostGloas,
81
+ } from "./interface.js";
74
82
 
75
83
  export class BeaconStateView implements IBeaconStateViewLatestFork {
76
84
  private readonly config: BeaconConfig;
@@ -108,6 +116,10 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
108
116
  return this.config.getForkName(this.cachedState.slot);
109
117
  }
110
118
 
119
+ get forkSeq(): ForkSeq {
120
+ return this.config.getForkSeq(this.cachedState.slot);
121
+ }
122
+
111
123
  get slot(): number {
112
124
  return this.cachedState.slot;
113
125
  }
@@ -444,6 +456,18 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
444
456
  }
445
457
  throw new Error(`PTC committees are not available for epoch=${epoch}`);
446
458
  }
459
+
460
+ /**
461
+ * Return the PTC for a slot in the previous, current or next epoch
462
+ */
463
+ getPayloadTimelinessCommittee(slot: Slot): Uint32Array {
464
+ if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) {
465
+ throw new Error("PTC committees are not supported before Gloas");
466
+ }
467
+
468
+ return (this.cachedState as CachedBeaconStateGloas).epochCtx.getPayloadTimelinessCommittee(slot);
469
+ }
470
+
447
471
  /**
448
472
  * Return all positions of the validator in the PTC committee for the given slot.
449
473
  *
@@ -823,6 +847,13 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
823
847
 
824
848
  // State transition
825
849
 
850
+ computeNewStateRoot({block}: ComputeNewStateRootInput, modules: StateTransitionModules): ComputeNewStateRootResult {
851
+ const postState = new BeaconStateView(
852
+ stateTransition(this.cachedState, block, computeNewStateRootStateTransitionOpts, modules)
853
+ );
854
+ return getComputeNewStateRootResult(postState, modules);
855
+ }
856
+
826
857
  stateTransition(
827
858
  signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock,
828
859
  options: StateTransitionOpts,
@@ -0,0 +1,35 @@
1
+ import {DataAvailabilityStatus, ExecutionPayloadStatus} from "../block/externalData.js";
2
+ import {StateHashTreeRootSource, StateTransitionModules, StateTransitionOpts} from "../stateTransition.js";
3
+ import {ComputeNewStateRootResult, IBeaconStateView} from "./interface.js";
4
+
5
+ /** State transition options for computing the state root of a locally produced block. */
6
+ export const computeNewStateRootStateTransitionOpts: StateTransitionOpts = {
7
+ // ExecutionPayloadStatus.valid: Assume payload valid, it has been produced by a trusted EL
8
+ executionPayloadStatus: ExecutionPayloadStatus.valid,
9
+ // DataAvailabilityStatus.available: Assume the blobs to be available, have just been produced by trusted EL
10
+ dataAvailabilityStatus: DataAvailabilityStatus.Available,
11
+ // verifyStateRoot: false | the root in the block is zero-ed, it's being computed here
12
+ verifyStateRoot: false,
13
+ // verifyProposer: false | as the block signature is zero-ed
14
+ verifyProposer: false,
15
+ // verifySignatures: false | since the data to assemble the block is trusted
16
+ verifySignatures: false,
17
+ // Preserve cache in source state, since the resulting state is not added to the state cache
18
+ dontTransferCache: true,
19
+ };
20
+
21
+ export function getComputeNewStateRootResult(
22
+ postState: IBeaconStateView,
23
+ {metrics}: StateTransitionModules
24
+ ): ComputeNewStateRootResult {
25
+ const {attestations, syncAggregate, slashing} = postState.proposerRewards;
26
+ const proposerReward = BigInt(attestations + syncAggregate + slashing);
27
+
28
+ const hashTreeRootTimer = metrics?.stateHashTreeRootTime.startTimer({
29
+ source: StateHashTreeRootSource.computeNewStateRoot,
30
+ });
31
+ const newStateRoot = postState.hashTreeRoot();
32
+ hashTreeRootTimer?.();
33
+
34
+ return {newStateRoot, proposerReward, postState};
35
+ }
@@ -10,6 +10,7 @@ import {
10
10
  ForkPostFulu,
11
11
  ForkPostGloas,
12
12
  ForkPostHeze,
13
+ ForkSeq,
13
14
  isForkPostAltair,
14
15
  isForkPostBellatrix,
15
16
  isForkPostCapella,
@@ -29,6 +30,7 @@ import {
29
30
  Epoch,
30
31
  ExecutionPayloadBid,
31
32
  ExecutionPayloadHeader,
33
+ Gwei,
32
34
  Root,
33
35
  RootHex,
34
36
  SignedBeaconBlock,
@@ -54,6 +56,20 @@ import {StateTransitionModules, StateTransitionOpts} from "../stateTransition.js
54
56
  import {EpochShuffling} from "../util/epochShuffling.js";
55
57
  import {PreVerifyBuilderDepositsResult} from "../util/preVerifyBuilderDeposits.js";
56
58
 
59
+ /** Inputs for computing the state root of a locally produced block. */
60
+ export type ComputeNewStateRootInput = {
61
+ block: SignedBeaconBlock | SignedBlindedBeaconBlock;
62
+ /** Pre-serialized block bytes for native implementations. */
63
+ ssz?: Uint8Array;
64
+ };
65
+
66
+ /** State root computation result. Includes data derived from the post-state. */
67
+ export type ComputeNewStateRootResult = {
68
+ newStateRoot: Root;
69
+ proposerReward: Gwei;
70
+ postState: IBeaconStateView;
71
+ };
72
+
57
73
  /**
58
74
  * A read-only view of the BeaconState.
59
75
  */
@@ -62,6 +78,7 @@ export interface IBeaconStateView {
62
78
 
63
79
  // phase0
64
80
  forkName: ForkName;
81
+ forkSeq: ForkSeq;
65
82
  slot: Slot;
66
83
  fork: Fork;
67
84
  epoch: Epoch;
@@ -161,6 +178,7 @@ export interface IBeaconStateView {
161
178
  hashTreeRoot(): Uint8Array;
162
179
 
163
180
  // State transition
181
+ computeNewStateRoot(input: ComputeNewStateRootInput, modules: StateTransitionModules): ComputeNewStateRootResult;
164
182
  stateTransition(
165
183
  signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock,
166
184
  options: StateTransitionOpts,
@@ -267,6 +285,7 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu {
267
285
  getBuildersLength(): number;
268
286
  canBuilderCoverBid(builderIndex: BuilderIndex, bidAmount: number): boolean;
269
287
  getEpochPTCs(epoch: Epoch): Uint32Array[];
288
+ getPayloadTimelinessCommittee(slot: Slot): Uint32Array;
270
289
  getIndicesInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number[];
271
290
  /**
272
291
  * Clone the state and apply parent execution payload effects.
@@ -308,13 +327,20 @@ export type IBeaconStateViewLatestFork = Omit<
308
327
  * `loadOtherState`, `withParentPayloadApplied`) return `IBeaconStateViewNative`
309
328
  * so callers can re-wrap without an `as unknown` cast. Param lists are reused
310
329
  * via `Parameters<...>` to avoid duplicating signatures.
330
+ * - `computeNewStateRoot` is implemented by the wrapper because its inputs differ
331
+ * between the TypeScript and Zig implementations.
311
332
  *
312
333
  * The TS-side `BeaconStateView` also structurally satisfies this contract since
313
334
  * `BitArray` exposes `uint8Array` and `bitLen`.
314
335
  */
315
336
  export type IBeaconStateViewNative = Omit<
316
337
  IBeaconStateViewLatestFork,
317
- "executionPayloadAvailability" | "loadOtherState" | "stateTransition" | "processSlots" | "withParentPayloadApplied"
338
+ | "computeNewStateRoot"
339
+ | "executionPayloadAvailability"
340
+ | "loadOtherState"
341
+ | "stateTransition"
342
+ | "processSlots"
343
+ | "withParentPayloadApplied"
318
344
  > & {
319
345
  executionPayloadAvailability: {uint8Array: Uint8Array; bitLen: number};
320
346
  loadOtherState(...args: Parameters<IBeaconStateViewLatestFork["loadOtherState"]>): IBeaconStateViewNative;
@@ -1,6 +1,6 @@
1
1
  import {CompactMultiProof} from "@chainsafe/persistent-merkle-tree";
2
2
  import {BitArray, ByteViews} from "@chainsafe/ssz";
3
- import {ForkName} from "@lodestar/params";
3
+ import {ForkName, ForkSeq} from "@lodestar/params";
4
4
  import {
5
5
  BeaconBlock,
6
6
  BeaconState,
@@ -36,7 +36,10 @@ import {SyncCommitteeWitness} from "../lightClient/types.js";
36
36
  import {StateTransitionModules, StateTransitionOpts} from "../stateTransition.js";
37
37
  import {EpochShuffling} from "../util/epochShuffling.js";
38
38
  import {PreVerifyBuilderDepositsResult} from "../util/preVerifyBuilderDeposits.js";
39
+ import {computeNewStateRootStateTransitionOpts, getComputeNewStateRootResult} from "./computeNewStateRoot.js";
39
40
  import {
41
+ ComputeNewStateRootInput,
42
+ ComputeNewStateRootResult,
40
43
  IBeaconStateView,
41
44
  IBeaconStateViewGloas,
42
45
  IBeaconStateViewLatestFork,
@@ -63,6 +66,7 @@ import {
63
66
  export class NativeBeaconStateView implements IBeaconStateViewLatestFork {
64
67
  // phase0
65
68
  private _forkName: ForkName | null = null;
69
+ private _forkSeq: ForkSeq | null = null;
66
70
  private _slot: Slot | null = null;
67
71
  private _fork: Fork | null = null;
68
72
  private _epoch: Epoch | null = null;
@@ -191,6 +195,13 @@ export class NativeBeaconStateView implements IBeaconStateViewLatestFork {
191
195
  return this._forkName;
192
196
  }
193
197
 
198
+ get forkSeq(): ForkSeq {
199
+ if (this._forkSeq === null) {
200
+ this._forkSeq = this.binding.forkSeq;
201
+ }
202
+ return this._forkSeq;
203
+ }
204
+
194
205
  get slot(): Slot {
195
206
  if (this._slot === null) {
196
207
  this._slot = this.binding.slot;
@@ -637,6 +648,13 @@ export class NativeBeaconStateView implements IBeaconStateViewLatestFork {
637
648
 
638
649
  // State transition
639
650
 
651
+ computeNewStateRoot(input: ComputeNewStateRootInput, modules: StateTransitionModules): ComputeNewStateRootResult {
652
+ const postState = new NativeBeaconStateView(
653
+ this.binding.stateTransition(input.block, computeNewStateRootStateTransitionOpts, modules)
654
+ );
655
+ return getComputeNewStateRootResult(postState, modules);
656
+ }
657
+
640
658
  stateTransition(
641
659
  signedBlock: SignedBeaconBlock | SignedBlindedBeaconBlock,
642
660
  options: StateTransitionOpts,
@@ -908,6 +926,10 @@ export class NativeBeaconStateView implements IBeaconStateViewLatestFork {
908
926
  return cached;
909
927
  }
910
928
 
929
+ getPayloadTimelinessCommittee(slot: Slot): Uint32Array {
930
+ return this.binding.getPayloadTimelinessCommittee(slot);
931
+ }
932
+
911
933
  getIndicesInPayloadTimelinessCommittee(validatorIndex: ValidatorIndex, slot: Slot): number[] {
912
934
  return this.binding.getIndicesInPayloadTimelinessCommittee(validatorIndex, slot);
913
935
  }
package/src/util/gloas.ts CHANGED
@@ -220,6 +220,7 @@ export function initializePtcWindow(state: CachedBeaconStateFulu): Uint32Array[]
220
220
  state,
221
221
  epoch,
222
222
  shuffling.committees,
223
+ shuffling.shuffling,
223
224
  state.epochCtx.effectiveBalanceIncrements
224
225
  )
225
226
  );
package/src/util/seed.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import {digest} from "@chainsafe/as-sha256";
2
+ import {computePtcIndicesForEpoch} from "@chainsafe/lodestar-z/shuffle";
2
3
  import {
3
4
  computeProposerIndex as nativeComputeProposerIndex,
4
5
  computeSyncCommitteeIndices as nativeComputeSyncCommitteeIndices,
@@ -275,25 +276,36 @@ export function computePayloadTimelinessCommitteesForEpoch(
275
276
  state: BeaconStateAllForks,
276
277
  epoch: number,
277
278
  committees: Uint32Array[][],
279
+ shuffling: Uint32Array,
278
280
  effectiveBalanceIncrements: EffectiveBalanceIncrements
279
281
  ): Uint32Array[] {
280
282
  const epochSeed = getSeed(state, epoch, DOMAIN_PTC_ATTESTER);
281
283
  const startSlot = epoch * SLOTS_PER_EPOCH;
284
+ const slotOffsets = new Uint32Array(SLOTS_PER_EPOCH + 1);
285
+ let shufflingLength = 0;
286
+ for (let i = 0; i < SLOTS_PER_EPOCH; i++) {
287
+ slotOffsets[i] = shufflingLength;
288
+ for (const committee of committees[i]) {
289
+ shufflingLength += committee.length;
290
+ }
291
+ }
292
+ slotOffsets[SLOTS_PER_EPOCH] = shufflingLength;
293
+
294
+ // Native sampling returns all committees concatenated in slot order, with PTC_SIZE indices per slot.
295
+ const flatPtcIndices = computePtcIndicesForEpoch(
296
+ epochSeed,
297
+ startSlot,
298
+ SLOTS_PER_EPOCH,
299
+ shuffling,
300
+ slotOffsets,
301
+ effectiveBalanceIncrements,
302
+ PTC_SIZE,
303
+ MAX_EFFECTIVE_BALANCE_ELECTRA,
304
+ EFFECTIVE_BALANCE_INCREMENT
305
+ );
282
306
  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
307
  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);
308
+ result[i] = flatPtcIndices.subarray(i * PTC_SIZE, (i + 1) * PTC_SIZE);
297
309
  }
298
310
  return result;
299
311
  }