@lodestar/state-transition 1.43.0-dev.4358217e12 → 1.43.0-dev.4451fec75a

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 (79) hide show
  1. package/lib/block/processConsolidationRequest.d.ts.map +1 -1
  2. package/lib/block/processConsolidationRequest.js +2 -1
  3. package/lib/block/processConsolidationRequest.js.map +1 -1
  4. package/lib/block/processParentExecutionPayload.d.ts +2 -2
  5. package/lib/block/processParentExecutionPayload.js +3 -3
  6. package/lib/block/processWithdrawals.d.ts.map +1 -1
  7. package/lib/block/processWithdrawals.js +2 -4
  8. package/lib/block/processWithdrawals.js.map +1 -1
  9. package/lib/cache/epochCache.js +3 -3
  10. package/lib/cache/epochCache.js.map +1 -1
  11. package/lib/epoch/processPendingDeposits.d.ts.map +1 -1
  12. package/lib/epoch/processPendingDeposits.js +4 -2
  13. package/lib/epoch/processPendingDeposits.js.map +1 -1
  14. package/lib/lightClient/spec/index.d.ts +22 -0
  15. package/lib/lightClient/spec/index.d.ts.map +1 -0
  16. package/lib/lightClient/spec/index.js +58 -0
  17. package/lib/lightClient/spec/index.js.map +1 -0
  18. package/lib/lightClient/spec/isBetterUpdate.d.ts +23 -0
  19. package/lib/lightClient/spec/isBetterUpdate.d.ts.map +1 -0
  20. package/lib/lightClient/spec/isBetterUpdate.js +66 -0
  21. package/lib/lightClient/spec/isBetterUpdate.js.map +1 -0
  22. package/lib/lightClient/spec/processLightClientUpdate.d.ts +12 -0
  23. package/lib/lightClient/spec/processLightClientUpdate.d.ts.map +1 -0
  24. package/lib/lightClient/spec/processLightClientUpdate.js +80 -0
  25. package/lib/lightClient/spec/processLightClientUpdate.js.map +1 -0
  26. package/lib/lightClient/spec/store.d.ts +45 -0
  27. package/lib/lightClient/spec/store.d.ts.map +1 -0
  28. package/lib/lightClient/spec/store.js +56 -0
  29. package/lib/lightClient/spec/store.js.map +1 -0
  30. package/lib/lightClient/spec/utils.d.ts +47 -0
  31. package/lib/lightClient/spec/utils.d.ts.map +1 -0
  32. package/lib/lightClient/spec/utils.js +197 -0
  33. package/lib/lightClient/spec/utils.js.map +1 -0
  34. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts +4 -0
  35. package/lib/lightClient/spec/validateLightClientBootstrap.d.ts.map +1 -0
  36. package/lib/lightClient/spec/validateLightClientBootstrap.js +22 -0
  37. package/lib/lightClient/spec/validateLightClientBootstrap.js.map +1 -0
  38. package/lib/lightClient/spec/validateLightClientUpdate.d.ts +5 -0
  39. package/lib/lightClient/spec/validateLightClientUpdate.d.ts.map +1 -0
  40. package/lib/lightClient/spec/validateLightClientUpdate.js +88 -0
  41. package/lib/lightClient/spec/validateLightClientUpdate.js.map +1 -0
  42. package/lib/slot/upgradeStateToElectra.d.ts.map +1 -1
  43. package/lib/slot/upgradeStateToElectra.js +2 -2
  44. package/lib/slot/upgradeStateToElectra.js.map +1 -1
  45. package/lib/stateView/beaconStateView.d.ts +14 -5
  46. package/lib/stateView/beaconStateView.d.ts.map +1 -1
  47. package/lib/stateView/beaconStateView.js +40 -11
  48. package/lib/stateView/beaconStateView.js.map +1 -1
  49. package/lib/stateView/interface.d.ts +7 -5
  50. package/lib/stateView/interface.d.ts.map +1 -1
  51. package/lib/stateView/interface.js.map +1 -1
  52. package/lib/util/epoch.d.ts.map +1 -1
  53. package/lib/util/epoch.js +6 -4
  54. package/lib/util/epoch.js.map +1 -1
  55. package/lib/util/loadState/loadState.js +4 -4
  56. package/lib/util/loadState/loadState.js.map +1 -1
  57. package/lib/util/validator.d.ts +14 -2
  58. package/lib/util/validator.d.ts.map +1 -1
  59. package/lib/util/validator.js +24 -2
  60. package/lib/util/validator.js.map +1 -1
  61. package/package.json +13 -8
  62. package/src/block/processConsolidationRequest.ts +2 -1
  63. package/src/block/processParentExecutionPayload.ts +3 -3
  64. package/src/block/processWithdrawals.ts +2 -4
  65. package/src/cache/epochCache.ts +3 -3
  66. package/src/epoch/processPendingDeposits.ts +5 -2
  67. package/src/lightClient/spec/index.ts +101 -0
  68. package/src/lightClient/spec/isBetterUpdate.ts +94 -0
  69. package/src/lightClient/spec/processLightClientUpdate.ts +119 -0
  70. package/src/lightClient/spec/store.ts +106 -0
  71. package/src/lightClient/spec/utils.ts +317 -0
  72. package/src/lightClient/spec/validateLightClientBootstrap.ts +39 -0
  73. package/src/lightClient/spec/validateLightClientUpdate.ts +145 -0
  74. package/src/slot/upgradeStateToElectra.ts +4 -2
  75. package/src/stateView/beaconStateView.ts +43 -12
  76. package/src/stateView/interface.ts +7 -5
  77. package/src/util/epoch.ts +13 -4
  78. package/src/util/loadState/loadState.ts +4 -4
  79. package/src/util/validator.ts +42 -2
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/ChainSafe/lodestar/issues"
13
13
  },
14
- "version": "1.43.0-dev.4358217e12",
14
+ "version": "1.43.0-dev.4451fec75a",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
@@ -34,6 +34,11 @@
34
34
  "types": "./lib/slot/index.d.ts",
35
35
  "import": "./lib/slot/index.js"
36
36
  },
37
+ "./light-client": {
38
+ "bun": "./src/lightClient/spec/index.ts",
39
+ "types": "./lib/lightClient/spec/index.d.ts",
40
+ "import": "./lib/lightClient/spec/index.js"
41
+ },
37
42
  "./test-utils": {
38
43
  "bun": "./src/testUtils/index.ts",
39
44
  "types": "./lib/testUtils/index.d.ts",
@@ -62,19 +67,19 @@
62
67
  "dependencies": {
63
68
  "@chainsafe/as-sha256": "^1.2.0",
64
69
  "@chainsafe/blst": "^2.2.0",
65
- "@chainsafe/persistent-merkle-tree": "^1.2.1",
70
+ "@chainsafe/persistent-merkle-tree": "^1.2.5",
66
71
  "@chainsafe/persistent-ts": "^1.0.0",
67
72
  "@chainsafe/pubkey-index-map": "^3.0.0",
68
73
  "@chainsafe/ssz": "^1.4.0",
69
74
  "@chainsafe/swap-or-not-shuffle": "^1.2.1",
70
- "@lodestar/config": "^1.43.0-dev.4358217e12",
71
- "@lodestar/params": "^1.43.0-dev.4358217e12",
72
- "@lodestar/types": "^1.43.0-dev.4358217e12",
73
- "@lodestar/utils": "^1.43.0-dev.4358217e12",
75
+ "@lodestar/config": "^1.43.0-dev.4451fec75a",
76
+ "@lodestar/params": "^1.43.0-dev.4451fec75a",
77
+ "@lodestar/types": "^1.43.0-dev.4451fec75a",
78
+ "@lodestar/utils": "^1.43.0-dev.4451fec75a",
74
79
  "@vekexasia/bigint-buffer2": "^1.1.1"
75
80
  },
76
81
  "devDependencies": {
77
- "@lodestar/api": "^1.43.0-dev.4358217e12"
82
+ "@lodestar/api": "^1.43.0-dev.4451fec75a"
78
83
  },
79
84
  "keywords": [
80
85
  "ethereum",
@@ -82,5 +87,5 @@
82
87
  "beacon",
83
88
  "blockchain"
84
89
  ],
85
- "gitHead": "b573e546006ca69dde6b249cb32a72502eff9f3d"
90
+ "gitHead": "69d7d9c6e242405ae8c43fdb382e166b74bba752"
86
91
  }
@@ -46,7 +46,8 @@ export function processConsolidationRequest(
46
46
  }
47
47
 
48
48
  // If there is too little available consolidation churn limit, consolidation requests are ignored
49
- if (getConsolidationChurnLimit(state.epochCtx) <= MIN_ACTIVATION_BALANCE) {
49
+ const fork = state.config.getForkSeq(state.slot);
50
+ if (getConsolidationChurnLimit(fork, state.epochCtx) <= MIN_ACTIVATION_BALANCE) {
50
51
  return;
51
52
  }
52
53
 
@@ -10,7 +10,7 @@ import {processWithdrawalRequest} from "./processWithdrawalRequest.js";
10
10
  /**
11
11
  * Process parent execution payload effects as the first step of processBlock.
12
12
  *
13
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/beacon-chain.md#new-process_parent_execution_payload
13
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-process_parent_execution_payload
14
14
  */
15
15
  export function processParentExecutionPayload(state: CachedBeaconStateGloas, block: BeaconBlock<ForkPostGloas>): void {
16
16
  const bid = block.body.signedExecutionPayloadBid.message;
@@ -42,7 +42,7 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo
42
42
  * Called from processParentExecutionPayload during block processing, and from the validator during
43
43
  * block production before computing withdrawals.
44
44
  *
45
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/beacon-chain.md#new-apply_parent_execution_payload
45
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-apply_parent_execution_payload
46
46
  */
47
47
  export function applyParentExecutionPayload(state: CachedBeaconStateGloas, requests: electra.ExecutionRequests): void {
48
48
  const fork = state.config.getForkSeq(state.slot);
@@ -94,7 +94,7 @@ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, reque
94
94
  * Settle a builder payment at the given index: move its withdrawal (if any) to the
95
95
  * pending withdrawals list and clear the payment slot.
96
96
  *
97
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/beacon-chain.md#new-settle_builder_payment
97
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-settle_builder_payment
98
98
  */
99
99
  function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: number): void {
100
100
  if (paymentIndex >= state.builderPendingPayments.length) {
@@ -10,7 +10,6 @@ import {
10
10
  } from "@lodestar/params";
11
11
  import {BuilderIndex, ValidatorIndex, capella, ssz} from "@lodestar/types";
12
12
  import {byteArrayEquals, toRootHex} from "@lodestar/utils";
13
- import {ZERO_HASH} from "../constants/index.js";
14
13
  import {CachedBeaconStateCapella, CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
15
14
  import {
16
15
  convertBuilderIndexToValidatorIndex,
@@ -31,15 +30,14 @@ export function processWithdrawals(
31
30
  state: CachedBeaconStateCapella | CachedBeaconStateElectra | CachedBeaconStateGloas,
32
31
  payload?: capella.FullOrBlindedExecutionPayload
33
32
  ): void {
34
- // Return early if this is genesis block or the parent block is empty
33
+ // Return early if the parent block is empty
35
34
  if (fork >= ForkSeq.gloas) {
36
35
  const stateGloas = state as CachedBeaconStateGloas;
37
- const isGenesisBlock = byteArrayEquals(stateGloas.latestBlockHash, ZERO_HASH);
38
36
  const isParentBlockEmpty = !byteArrayEquals(
39
37
  stateGloas.latestBlockHash,
40
38
  stateGloas.latestExecutionPayloadBid.blockHash
41
39
  );
42
- if (isGenesisBlock || isParentBlockEmpty) {
40
+ if (isParentBlockEmpty) {
43
41
  return;
44
42
  }
45
43
  }
@@ -38,9 +38,9 @@ import {
38
38
  computeEpochAtSlot,
39
39
  computeProposers,
40
40
  computeSyncPeriodAtEpoch,
41
- getActivationChurnLimit,
42
41
  getChurnLimit,
43
42
  getSeed,
43
+ getValidatorActivationChurnLimit,
44
44
  isActiveValidator,
45
45
  isAggregatorFromCommitteeLength,
46
46
  } from "../util/index.js";
@@ -478,7 +478,7 @@ export class EpochCache {
478
478
  // the first block of the epoch process_block() call. So churnLimit must be computed at the end of the before epoch
479
479
  // transition and the result is valid until the end of the next epoch transition
480
480
  const churnLimit = getChurnLimit(config, currentShuffling.activeIndices.length);
481
- const activationChurnLimit = getActivationChurnLimit(
481
+ const activationChurnLimit = getValidatorActivationChurnLimit(
482
482
  config,
483
483
  config.getForkSeq(state.slot),
484
484
  currentShuffling.activeIndices.length
@@ -652,7 +652,7 @@ export class EpochCache {
652
652
  // the first block of the epoch process_block() call. So churnLimit must be computed at the end of the before epoch
653
653
  // transition and the result is valid until the end of the next epoch transition
654
654
  this.churnLimit = getChurnLimit(this.config, this.currentShuffling.activeIndices.length);
655
- this.activationChurnLimit = getActivationChurnLimit(
655
+ this.activationChurnLimit = getValidatorActivationChurnLimit(
656
656
  this.config,
657
657
  this.config.getForkSeq(state.slot),
658
658
  this.currentShuffling.activeIndices.length
@@ -5,7 +5,7 @@ import {CachedBeaconStateElectra, EpochTransitionCache} from "../types.js";
5
5
  import {increaseBalance} from "../util/balance.js";
6
6
  import {hasCompoundingWithdrawalCredential, isValidatorKnown} from "../util/electra.js";
7
7
  import {computeStartSlotAtEpoch} from "../util/epoch.js";
8
- import {getActivationExitChurnLimit} from "../util/validator.js";
8
+ import {getActivationChurnLimit, getActivationExitChurnLimit} from "../util/validator.js";
9
9
 
10
10
  /**
11
11
  * Starting from Electra:
@@ -17,8 +17,11 @@ import {getActivationExitChurnLimit} from "../util/validator.js";
17
17
  * TODO Electra: Update ssz library to support batch push to `pendingDeposits`
18
18
  */
19
19
  export function processPendingDeposits(state: CachedBeaconStateElectra, cache: EpochTransitionCache): void {
20
+ const fork = state.config.getForkSeq(state.slot);
20
21
  const nextEpoch = state.epochCtx.epoch + 1;
21
- const availableForProcessing = state.depositBalanceToConsume + BigInt(getActivationExitChurnLimit(state.epochCtx));
22
+ const churnLimit =
23
+ fork >= ForkSeq.gloas ? getActivationChurnLimit(state.epochCtx) : getActivationExitChurnLimit(state.epochCtx);
24
+ const availableForProcessing = state.depositBalanceToConsume + BigInt(churnLimit);
22
25
  let processedAmount = 0;
23
26
  let nextDepositIndex = 0;
24
27
  const depositsToPostpone = [];
@@ -0,0 +1,101 @@
1
+ import {BeaconConfig} from "@lodestar/config";
2
+ import {UPDATE_TIMEOUT} from "@lodestar/params";
3
+ import {
4
+ LightClientBootstrap,
5
+ LightClientFinalityUpdate,
6
+ LightClientOptimisticUpdate,
7
+ LightClientUpdate,
8
+ Slot,
9
+ } from "@lodestar/types";
10
+ import {computeSyncPeriodAtSlot} from "../../util/epoch.js";
11
+ import {
12
+ type ProcessUpdateOpts,
13
+ getSyncCommitteeAtPeriod,
14
+ processLightClientUpdate,
15
+ } from "./processLightClientUpdate.js";
16
+ import {type ILightClientStore, LightClientStore, type LightClientStoreEvents} from "./store.js";
17
+ import {ZERO_HEADER, ZERO_SYNC_COMMITTEE, getZeroFinalityBranch, getZeroSyncCommitteeBranch} from "./utils.js";
18
+
19
+ export type {LightClientUpdateSummary} from "./isBetterUpdate.js";
20
+ export {isBetterUpdate, toLightClientUpdateSummary} from "./isBetterUpdate.js";
21
+ export {
22
+ type ProcessUpdateOpts,
23
+ getSyncCommitteeAtPeriod,
24
+ isSafeLightClientUpdate,
25
+ processLightClientUpdate,
26
+ } from "./processLightClientUpdate.js";
27
+ export {
28
+ type ILightClientStore,
29
+ LightClientStore,
30
+ type LightClientStoreEvents,
31
+ type LightClientUpdateWithSummary,
32
+ type SyncCommitteeFast,
33
+ } from "./store.js";
34
+ export {
35
+ getSafetyThreshold,
36
+ isFinalityUpdate,
37
+ isSyncCommitteeUpdate,
38
+ isValidLightClientHeader,
39
+ normalizeMerkleBranch,
40
+ upgradeLightClientFinalityUpdate,
41
+ upgradeLightClientHeader,
42
+ upgradeLightClientOptimisticUpdate,
43
+ upgradeLightClientStore,
44
+ upgradeLightClientUpdate,
45
+ } from "./utils.js";
46
+ export {validateLightClientBootstrap} from "./validateLightClientBootstrap.js";
47
+ export {validateLightClientUpdate} from "./validateLightClientUpdate.js";
48
+
49
+ export class LightclientSpec {
50
+ readonly store: ILightClientStore;
51
+ readonly config: BeaconConfig;
52
+
53
+ constructor(
54
+ config: BeaconConfig,
55
+ private readonly opts: ProcessUpdateOpts & LightClientStoreEvents,
56
+ bootstrap: LightClientBootstrap
57
+ ) {
58
+ this.store = new LightClientStore(config, bootstrap, opts);
59
+ this.config = config;
60
+ }
61
+
62
+ onUpdate(currentSlot: Slot, update: LightClientUpdate): void {
63
+ processLightClientUpdate(this.config, this.store, currentSlot, this.opts, update);
64
+ }
65
+
66
+ onFinalityUpdate(currentSlot: Slot, finalityUpdate: LightClientFinalityUpdate): void {
67
+ this.onUpdate(currentSlot, {
68
+ attestedHeader: finalityUpdate.attestedHeader,
69
+ nextSyncCommittee: ZERO_SYNC_COMMITTEE,
70
+ nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(this.config.getForkName(finalityUpdate.signatureSlot)),
71
+ finalizedHeader: finalityUpdate.finalizedHeader,
72
+ finalityBranch: finalityUpdate.finalityBranch,
73
+ syncAggregate: finalityUpdate.syncAggregate,
74
+ signatureSlot: finalityUpdate.signatureSlot,
75
+ });
76
+ }
77
+
78
+ onOptimisticUpdate(currentSlot: Slot, optimisticUpdate: LightClientOptimisticUpdate): void {
79
+ this.onUpdate(currentSlot, {
80
+ attestedHeader: optimisticUpdate.attestedHeader,
81
+ nextSyncCommittee: ZERO_SYNC_COMMITTEE,
82
+ nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(this.config.getForkName(optimisticUpdate.signatureSlot)),
83
+ finalizedHeader: {beacon: ZERO_HEADER},
84
+ finalityBranch: getZeroFinalityBranch(this.config.getForkName(optimisticUpdate.signatureSlot)),
85
+ syncAggregate: optimisticUpdate.syncAggregate,
86
+ signatureSlot: optimisticUpdate.signatureSlot,
87
+ });
88
+ }
89
+
90
+ forceUpdate(currentSlot: Slot): void {
91
+ for (const bestValidUpdate of this.store.bestValidUpdates.values()) {
92
+ if (currentSlot > bestValidUpdate.update.finalizedHeader.beacon.slot + UPDATE_TIMEOUT) {
93
+ const updatePeriod = computeSyncPeriodAtSlot(bestValidUpdate.update.signatureSlot);
94
+ // Simulate process_light_client_store_force_update() by forcing to apply a bestValidUpdate
95
+ // https://github.com/ethereum/consensus-specs/blob/a57e15636013eeba3610ff3ade41781dba1bb0cd/specs/altair/light-client/sync-protocol.md?plain=1#L394
96
+ // Call for `updatePeriod + 1` to force the update at `update.signatureSlot` to be applied
97
+ getSyncCommitteeAtPeriod(this.store, updatePeriod + 1, this.opts);
98
+ }
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,94 @@
1
+ import {SYNC_COMMITTEE_SIZE} from "@lodestar/params";
2
+ import {LightClientUpdate, Slot} from "@lodestar/types";
3
+ import {computeSyncPeriodAtSlot} from "../../util/epoch.js";
4
+ import {isFinalityUpdate, isSyncCommitteeUpdate, sumBits} from "./utils.js";
5
+
6
+ /**
7
+ * Wrapper type for `isBetterUpdate()` so we can apply its logic without requiring the full LightClientUpdate type.
8
+ */
9
+ export type LightClientUpdateSummary = {
10
+ activeParticipants: number;
11
+ attestedHeaderSlot: Slot;
12
+ signatureSlot: Slot;
13
+ finalizedHeaderSlot: Slot;
14
+ /** `if update.next_sync_committee_branch != [Bytes32() for _ in range(floorlog2(NEXT_SYNC_COMMITTEE_INDEX))]` */
15
+ isSyncCommitteeUpdate: boolean;
16
+ /** `if update.finality_branch != [Bytes32() for _ in range(floorlog2(FINALIZED_ROOT_INDEX))]` */
17
+ isFinalityUpdate: boolean;
18
+ };
19
+
20
+ /**
21
+ * Returns the update with more bits. On ties, prevUpdate is the better
22
+ *
23
+ * https://github.com/ethereum/consensus-specs/blob/be3c774069e16e89145660be511c1b183056017e/specs/altair/light-client/sync-protocol.md#is_better_update
24
+ */
25
+ export function isBetterUpdate(newUpdate: LightClientUpdateSummary, oldUpdate: LightClientUpdateSummary): boolean {
26
+ // Compare supermajority (> 2/3) sync committee participation
27
+ const newNumActiveParticipants = newUpdate.activeParticipants;
28
+ const oldNumActiveParticipants = oldUpdate.activeParticipants;
29
+ const newHasSupermajority = newNumActiveParticipants * 3 >= SYNC_COMMITTEE_SIZE * 2;
30
+ const oldHasSupermajority = oldNumActiveParticipants * 3 >= SYNC_COMMITTEE_SIZE * 2;
31
+ if (newHasSupermajority !== oldHasSupermajority) {
32
+ return newHasSupermajority;
33
+ }
34
+ if (!newHasSupermajority && newNumActiveParticipants !== oldNumActiveParticipants) {
35
+ return newNumActiveParticipants > oldNumActiveParticipants;
36
+ }
37
+
38
+ // Compare presence of relevant sync committee
39
+ const newHasRelevantSyncCommittee =
40
+ newUpdate.isSyncCommitteeUpdate &&
41
+ computeSyncPeriodAtSlot(newUpdate.attestedHeaderSlot) === computeSyncPeriodAtSlot(newUpdate.signatureSlot);
42
+ const oldHasRelevantSyncCommittee =
43
+ oldUpdate.isSyncCommitteeUpdate &&
44
+ computeSyncPeriodAtSlot(oldUpdate.attestedHeaderSlot) === computeSyncPeriodAtSlot(oldUpdate.signatureSlot);
45
+ if (newHasRelevantSyncCommittee !== oldHasRelevantSyncCommittee) {
46
+ return newHasRelevantSyncCommittee;
47
+ }
48
+
49
+ // Compare indication of any finality
50
+ const newHasFinality = newUpdate.isFinalityUpdate;
51
+ const oldHasFinality = oldUpdate.isFinalityUpdate;
52
+ if (newHasFinality !== oldHasFinality) {
53
+ return newHasFinality;
54
+ }
55
+
56
+ // Compare sync committee finality
57
+ if (newHasFinality) {
58
+ const newHasSyncCommitteeFinality =
59
+ computeSyncPeriodAtSlot(newUpdate.finalizedHeaderSlot) === computeSyncPeriodAtSlot(newUpdate.attestedHeaderSlot);
60
+ const oldHasSyncCommitteeFinality =
61
+ computeSyncPeriodAtSlot(oldUpdate.finalizedHeaderSlot) === computeSyncPeriodAtSlot(oldUpdate.attestedHeaderSlot);
62
+ if (newHasSyncCommitteeFinality !== oldHasSyncCommitteeFinality) {
63
+ return newHasSyncCommitteeFinality;
64
+ }
65
+ }
66
+
67
+ // Tiebreaker 1: Sync committee participation beyond supermajority
68
+ if (newNumActiveParticipants !== oldNumActiveParticipants) {
69
+ return newNumActiveParticipants > oldNumActiveParticipants;
70
+ }
71
+
72
+ // Tiebreaker 2: Prefer older data (fewer changes to best)
73
+ if (newUpdate.attestedHeaderSlot !== oldUpdate.attestedHeaderSlot) {
74
+ return newUpdate.attestedHeaderSlot < oldUpdate.attestedHeaderSlot;
75
+ }
76
+ return newUpdate.signatureSlot < oldUpdate.signatureSlot;
77
+ }
78
+
79
+ export function isSafeLightClientUpdate(update: LightClientUpdateSummary): boolean {
80
+ return (
81
+ update.activeParticipants * 3 >= SYNC_COMMITTEE_SIZE * 2 && update.isFinalityUpdate && update.isSyncCommitteeUpdate
82
+ );
83
+ }
84
+
85
+ export function toLightClientUpdateSummary(update: LightClientUpdate): LightClientUpdateSummary {
86
+ return {
87
+ activeParticipants: sumBits(update.syncAggregate.syncCommitteeBits),
88
+ attestedHeaderSlot: update.attestedHeader.beacon.slot,
89
+ signatureSlot: update.signatureSlot,
90
+ finalizedHeaderSlot: update.finalizedHeader.beacon.slot,
91
+ isSyncCommitteeUpdate: isSyncCommitteeUpdate(update),
92
+ isFinalityUpdate: isFinalityUpdate(update),
93
+ };
94
+ }
@@ -0,0 +1,119 @@
1
+ import {ChainForkConfig} from "@lodestar/config";
2
+ import {SYNC_COMMITTEE_SIZE} from "@lodestar/params";
3
+ import {LightClientUpdate, Slot, SyncPeriod} from "@lodestar/types";
4
+ import {pruneSetToMax} from "@lodestar/utils";
5
+ import {computeSyncPeriodAtSlot} from "../../util/epoch.js";
6
+ import {LightClientUpdateSummary, isBetterUpdate, toLightClientUpdateSummary} from "./isBetterUpdate.js";
7
+ import {type ILightClientStore, MAX_SYNC_PERIODS_CACHE, type SyncCommitteeFast} from "./store.js";
8
+ import {deserializeSyncCommittee, getSafetyThreshold, isSyncCommitteeUpdate, sumBits} from "./utils.js";
9
+ import {validateLightClientUpdate} from "./validateLightClientUpdate.js";
10
+
11
+ export interface ProcessUpdateOpts {
12
+ allowForcedUpdates?: boolean;
13
+ updateHeadersOnForcedUpdate?: boolean;
14
+ }
15
+
16
+ export function processLightClientUpdate(
17
+ config: ChainForkConfig,
18
+ store: ILightClientStore,
19
+ currentSlot: Slot,
20
+ opts: ProcessUpdateOpts,
21
+ update: LightClientUpdate
22
+ ): void {
23
+ if (update.signatureSlot > currentSlot) {
24
+ throw Error(`update slot ${update.signatureSlot} must not be in the future, current slot ${currentSlot}`);
25
+ }
26
+
27
+ const updateSignaturePeriod = computeSyncPeriodAtSlot(update.signatureSlot);
28
+ // TODO: Consider attempting to retrieve LightClientUpdate from transport if missing
29
+ // Note: store.getSyncCommitteeAtPeriod() may advance store
30
+ const syncCommittee = getSyncCommitteeAtPeriod(store, updateSignaturePeriod, opts);
31
+
32
+ validateLightClientUpdate(config, store, update, syncCommittee);
33
+
34
+ // Track the maximum number of active participants in the committee signatures
35
+ const syncCommitteeTrueBits = sumBits(update.syncAggregate.syncCommitteeBits);
36
+ store.setActiveParticipants(updateSignaturePeriod, syncCommitteeTrueBits);
37
+
38
+ // Update the optimistic header
39
+ if (
40
+ syncCommitteeTrueBits > getSafetyThreshold(store.getMaxActiveParticipants(updateSignaturePeriod)) &&
41
+ update.attestedHeader.beacon.slot > store.optimisticHeader.beacon.slot
42
+ ) {
43
+ store.optimisticHeader = update.attestedHeader;
44
+ }
45
+
46
+ // Update finalized header
47
+ if (
48
+ syncCommitteeTrueBits * 3 >= SYNC_COMMITTEE_SIZE * 2 &&
49
+ update.finalizedHeader.beacon.slot > store.finalizedHeader.beacon.slot
50
+ ) {
51
+ store.finalizedHeader = update.finalizedHeader;
52
+ if (store.finalizedHeader.beacon.slot > store.optimisticHeader.beacon.slot) {
53
+ store.optimisticHeader = store.finalizedHeader;
54
+ }
55
+ }
56
+
57
+ if (isSyncCommitteeUpdate(update)) {
58
+ // Update the best update in case we have to force-update to it if the timeout elapses
59
+ const bestValidUpdate = store.bestValidUpdates.get(updateSignaturePeriod);
60
+ const updateSummary = toLightClientUpdateSummary(update);
61
+ if (!bestValidUpdate || isBetterUpdate(updateSummary, bestValidUpdate.summary)) {
62
+ store.bestValidUpdates.set(updateSignaturePeriod, {update, summary: updateSummary});
63
+ pruneSetToMax(store.bestValidUpdates, MAX_SYNC_PERIODS_CACHE);
64
+ }
65
+
66
+ // Note: defer update next sync committee to a future getSyncCommitteeAtPeriod() call
67
+ }
68
+ }
69
+
70
+ export function getSyncCommitteeAtPeriod(
71
+ store: ILightClientStore,
72
+ period: SyncPeriod,
73
+ opts: ProcessUpdateOpts
74
+ ): SyncCommitteeFast {
75
+ const syncCommittee = store.syncCommittees.get(period);
76
+ if (syncCommittee) {
77
+ return syncCommittee;
78
+ }
79
+
80
+ const bestValidUpdate = store.bestValidUpdates.get(period - 1);
81
+ if (bestValidUpdate && (isSafeLightClientUpdate(bestValidUpdate.summary) || opts.allowForcedUpdates)) {
82
+ const {update} = bestValidUpdate;
83
+ const syncCommittee = deserializeSyncCommittee(update.nextSyncCommittee);
84
+ store.syncCommittees.set(period, syncCommittee);
85
+ pruneSetToMax(store.syncCommittees, MAX_SYNC_PERIODS_CACHE);
86
+ store.bestValidUpdates.delete(period - 1);
87
+
88
+ if (opts.updateHeadersOnForcedUpdate) {
89
+ // From https://github.com/ethereum/consensus-specs/blob/a57e15636013eeba3610ff3ade41781dba1bb0cd/specs/altair/light-client/sync-protocol.md?plain=1#L403
90
+ if (update.finalizedHeader.beacon.slot <= store.finalizedHeader.beacon.slot) {
91
+ update.finalizedHeader = update.attestedHeader;
92
+ }
93
+
94
+ // From https://github.com/ethereum/consensus-specs/blob/a57e15636013eeba3610ff3ade41781dba1bb0cd/specs/altair/light-client/sync-protocol.md?plain=1#L374
95
+ if (update.finalizedHeader.beacon.slot > store.finalizedHeader.beacon.slot) {
96
+ store.finalizedHeader = update.finalizedHeader;
97
+ }
98
+ if (store.finalizedHeader.beacon.slot > store.optimisticHeader.beacon.slot) {
99
+ store.optimisticHeader = store.finalizedHeader;
100
+ }
101
+ }
102
+
103
+ return syncCommittee;
104
+ }
105
+
106
+ const availableSyncCommittees = Array.from(store.syncCommittees.keys());
107
+ const availableBestValidUpdates = Array.from(store.bestValidUpdates.keys());
108
+ throw Error(
109
+ `No SyncCommittee for period ${period}` +
110
+ ` available syncCommittees ${JSON.stringify(availableSyncCommittees)}` +
111
+ ` available bestValidUpdates ${JSON.stringify(availableBestValidUpdates)}`
112
+ );
113
+ }
114
+
115
+ export function isSafeLightClientUpdate(update: LightClientUpdateSummary): boolean {
116
+ return (
117
+ update.activeParticipants * 3 >= SYNC_COMMITTEE_SIZE * 2 && update.isFinalityUpdate && update.isSyncCommitteeUpdate
118
+ );
119
+ }
@@ -0,0 +1,106 @@
1
+ import type {PublicKey} from "@chainsafe/blst";
2
+ import {BeaconConfig} from "@lodestar/config";
3
+ import {LightClientBootstrap, LightClientHeader, LightClientUpdate, SyncPeriod} from "@lodestar/types";
4
+ import {computeSyncPeriodAtSlot} from "../../util/epoch.js";
5
+ import type {LightClientUpdateSummary} from "./isBetterUpdate.js";
6
+ import {deserializeSyncCommittee} from "./utils.js";
7
+
8
+ export const MAX_SYNC_PERIODS_CACHE = 2;
9
+
10
+ export interface ILightClientStore {
11
+ readonly config: BeaconConfig;
12
+
13
+ /** Map of trusted SyncCommittee to be used for sig validation */
14
+ readonly syncCommittees: Map<SyncPeriod, SyncCommitteeFast>;
15
+ /** Map of best valid updates */
16
+ readonly bestValidUpdates: Map<SyncPeriod, LightClientUpdateWithSummary>;
17
+
18
+ getMaxActiveParticipants(period: SyncPeriod): number;
19
+ setActiveParticipants(period: SyncPeriod, activeParticipants: number): void;
20
+
21
+ // Header that is finalized
22
+ finalizedHeader: LightClientHeader;
23
+
24
+ // Most recent available reasonably-safe header
25
+ optimisticHeader: LightClientHeader;
26
+ }
27
+
28
+ export interface LightClientStoreEvents {
29
+ onSetFinalizedHeader?: (header: LightClientHeader) => void;
30
+ onSetOptimisticHeader?: (header: LightClientHeader) => void;
31
+ }
32
+
33
+ export class LightClientStore implements ILightClientStore {
34
+ readonly syncCommittees = new Map<SyncPeriod, SyncCommitteeFast>();
35
+ readonly bestValidUpdates = new Map<SyncPeriod, LightClientUpdateWithSummary>();
36
+
37
+ private finalizedHeaderValue: LightClientHeader;
38
+ private optimisticHeaderValue: LightClientHeader;
39
+
40
+ private readonly maxActiveParticipants = new Map<SyncPeriod, number>();
41
+
42
+ constructor(
43
+ readonly config: BeaconConfig,
44
+ bootstrap: LightClientBootstrap,
45
+ private readonly events: LightClientStoreEvents
46
+ ) {
47
+ const bootstrapPeriod = computeSyncPeriodAtSlot(bootstrap.header.beacon.slot);
48
+ this.syncCommittees.set(bootstrapPeriod, deserializeSyncCommittee(bootstrap.currentSyncCommittee));
49
+ this.finalizedHeaderValue = bootstrap.header;
50
+ this.optimisticHeaderValue = bootstrap.header;
51
+ }
52
+
53
+ get finalizedHeader(): LightClientHeader {
54
+ return this.finalizedHeaderValue;
55
+ }
56
+
57
+ set finalizedHeader(value: LightClientHeader) {
58
+ this.finalizedHeaderValue = value;
59
+ this.events.onSetFinalizedHeader?.(value);
60
+ }
61
+
62
+ get optimisticHeader(): LightClientHeader {
63
+ return this.optimisticHeaderValue;
64
+ }
65
+
66
+ set optimisticHeader(value: LightClientHeader) {
67
+ this.optimisticHeaderValue = value;
68
+ this.events.onSetOptimisticHeader?.(value);
69
+ }
70
+
71
+ getMaxActiveParticipants(period: SyncPeriod): number {
72
+ const currMaxParticipants = this.maxActiveParticipants.get(period) ?? 0;
73
+ const prevMaxParticipants = this.maxActiveParticipants.get(period - 1) ?? 0;
74
+
75
+ return Math.max(currMaxParticipants, prevMaxParticipants);
76
+ }
77
+
78
+ setActiveParticipants(period: SyncPeriod, activeParticipants: number): void {
79
+ const maxActiveParticipants = this.maxActiveParticipants.get(period) ?? 0;
80
+ if (activeParticipants > maxActiveParticipants) {
81
+ this.maxActiveParticipants.set(period, activeParticipants);
82
+ }
83
+
84
+ // Prune old entries
85
+ for (const key of this.maxActiveParticipants.keys()) {
86
+ if (key < period - MAX_SYNC_PERIODS_CACHE) {
87
+ this.maxActiveParticipants.delete(key);
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ export type SyncCommitteeFast = {
94
+ pubkeys: PublicKey[];
95
+ aggregatePubkey: PublicKey;
96
+ };
97
+
98
+ export type LightClientUpdateWithSummary = {
99
+ update: LightClientUpdate;
100
+ summary: LightClientUpdateSummary;
101
+ };
102
+
103
+ // === storePeriod ? store.currentSyncCommittee : store.nextSyncCommittee;
104
+ // if (!syncCommittee) {
105
+ // throw Error(`syncCommittee not available for signature period ${updateSignaturePeriod}`);
106
+ // }