@lodestar/state-transition 1.43.0 → 1.44.0-dev.1d0e0b9081
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.
- package/lib/block/processDepositRequest.d.ts +3 -11
- package/lib/block/processDepositRequest.d.ts.map +1 -1
- package/lib/block/processDepositRequest.js +27 -35
- package/lib/block/processDepositRequest.js.map +1 -1
- package/lib/block/processParentExecutionPayload.d.ts.map +1 -1
- package/lib/block/processParentExecutionPayload.js +4 -3
- package/lib/block/processParentExecutionPayload.js.map +1 -1
- package/lib/lightClient/spec/index.d.ts +22 -0
- package/lib/lightClient/spec/index.d.ts.map +1 -0
- package/lib/lightClient/spec/index.js +58 -0
- package/lib/lightClient/spec/index.js.map +1 -0
- package/lib/lightClient/spec/isBetterUpdate.d.ts +23 -0
- package/lib/lightClient/spec/isBetterUpdate.d.ts.map +1 -0
- package/lib/lightClient/spec/isBetterUpdate.js +66 -0
- package/lib/lightClient/spec/isBetterUpdate.js.map +1 -0
- package/lib/lightClient/spec/processLightClientUpdate.d.ts +12 -0
- package/lib/lightClient/spec/processLightClientUpdate.d.ts.map +1 -0
- package/lib/lightClient/spec/processLightClientUpdate.js +80 -0
- package/lib/lightClient/spec/processLightClientUpdate.js.map +1 -0
- package/lib/lightClient/spec/store.d.ts +45 -0
- package/lib/lightClient/spec/store.d.ts.map +1 -0
- package/lib/lightClient/spec/store.js +56 -0
- package/lib/lightClient/spec/store.js.map +1 -0
- package/lib/lightClient/spec/utils.d.ts +47 -0
- package/lib/lightClient/spec/utils.d.ts.map +1 -0
- package/lib/lightClient/spec/utils.js +197 -0
- package/lib/lightClient/spec/utils.js.map +1 -0
- package/lib/lightClient/spec/validateLightClientBootstrap.d.ts +4 -0
- package/lib/lightClient/spec/validateLightClientBootstrap.d.ts.map +1 -0
- package/lib/lightClient/spec/validateLightClientBootstrap.js +22 -0
- package/lib/lightClient/spec/validateLightClientBootstrap.js.map +1 -0
- package/lib/lightClient/spec/validateLightClientUpdate.d.ts +5 -0
- package/lib/lightClient/spec/validateLightClientUpdate.d.ts.map +1 -0
- package/lib/lightClient/spec/validateLightClientUpdate.js +88 -0
- package/lib/lightClient/spec/validateLightClientUpdate.js.map +1 -0
- package/lib/slot/upgradeStateToGloas.d.ts.map +1 -1
- package/lib/slot/upgradeStateToGloas.js +35 -29
- package/lib/slot/upgradeStateToGloas.js.map +1 -1
- package/lib/stateView/beaconStateView.d.ts +8 -3
- package/lib/stateView/beaconStateView.d.ts.map +1 -1
- package/lib/stateView/beaconStateView.js +15 -4
- package/lib/stateView/beaconStateView.js.map +1 -1
- package/lib/stateView/interface.d.ts +1 -1
- package/lib/stateView/interface.d.ts.map +1 -1
- package/lib/util/index.d.ts +1 -0
- package/lib/util/index.d.ts.map +1 -1
- package/lib/util/index.js +1 -0
- package/lib/util/index.js.map +1 -1
- package/lib/util/pendingDepositsLookup.d.ts +40 -0
- package/lib/util/pendingDepositsLookup.d.ts.map +1 -0
- package/lib/util/pendingDepositsLookup.js +84 -0
- package/lib/util/pendingDepositsLookup.js.map +1 -0
- package/lib/util/shuffling.d.ts +6 -5
- package/lib/util/shuffling.d.ts.map +1 -1
- package/lib/util/shuffling.js +13 -15
- package/lib/util/shuffling.js.map +1 -1
- package/package.json +12 -7
- package/src/block/processDepositRequest.ts +29 -47
- package/src/block/processParentExecutionPayload.ts +4 -3
- package/src/lightClient/spec/index.ts +101 -0
- package/src/lightClient/spec/isBetterUpdate.ts +94 -0
- package/src/lightClient/spec/processLightClientUpdate.ts +119 -0
- package/src/lightClient/spec/store.ts +106 -0
- package/src/lightClient/spec/utils.ts +317 -0
- package/src/lightClient/spec/validateLightClientBootstrap.ts +39 -0
- package/src/lightClient/spec/validateLightClientUpdate.ts +145 -0
- package/src/slot/upgradeStateToGloas.ts +43 -45
- package/src/stateView/beaconStateView.ts +15 -4
- package/src/stateView/interface.ts +1 -1
- package/src/util/index.ts +1 -0
- package/src/util/pendingDepositsLookup.ts +105 -0
- package/src/util/shuffling.ts +17 -15
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {BeaconConfig} from "@lodestar/config";
|
|
2
1
|
import {FAR_FUTURE_EPOCH, ForkSeq, UNSET_DEPOSIT_REQUESTS_START_INDEX} from "@lodestar/params";
|
|
3
|
-
import {BLSPubkey, Bytes32,
|
|
2
|
+
import {BLSPubkey, Bytes32, UintNum64, electra, ssz} from "@lodestar/types";
|
|
4
3
|
import {toPubkeyHex} from "@lodestar/utils";
|
|
5
4
|
import {CachedBeaconStateElectra, CachedBeaconStateGloas} from "../types.js";
|
|
6
5
|
import {findBuilderIndexByPubkey, isBuilderWithdrawalCredential} from "../util/gloas.js";
|
|
7
6
|
import {computeEpochAtSlot, isValidatorKnown} from "../util/index.js";
|
|
7
|
+
import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
|
|
8
8
|
import {isValidDepositSignature} from "./processDeposit.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -76,53 +76,60 @@ function addBuilderToRegistry(
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// TODO GLOAS:
|
|
80
|
-
//
|
|
81
|
-
//
|
|
79
|
+
// TODO GLOAS: the PendingDepositsLookup is currently scoped to a single envelope of
|
|
80
|
+
// deposit-requests. We can track it as ephemeral within EpochCache and transfer to the next block
|
|
81
|
+
// transition to reuse cached signature verifications.
|
|
82
82
|
// See https://github.com/ChainSafe/lodestar/issues/9181
|
|
83
83
|
export function processDepositRequest(
|
|
84
84
|
fork: ForkSeq,
|
|
85
85
|
state: CachedBeaconStateElectra | CachedBeaconStateGloas,
|
|
86
86
|
depositRequest: electra.DepositRequest,
|
|
87
|
-
|
|
87
|
+
pendingDepositsLookup?: PendingDepositsLookup
|
|
88
88
|
): void {
|
|
89
89
|
const {pubkey, withdrawalCredentials, amount, signature} = depositRequest;
|
|
90
90
|
|
|
91
|
-
// Check if this is a builder or validator deposit
|
|
92
91
|
if (fork >= ForkSeq.gloas) {
|
|
93
92
|
const stateGloas = state as CachedBeaconStateGloas;
|
|
94
|
-
const
|
|
95
|
-
pendingValidatorPubkeysCache ?? getPendingValidatorPubkeys(state.config, stateGloas);
|
|
93
|
+
const lookup = pendingDepositsLookup ?? PendingDepositsLookup.build(stateGloas);
|
|
96
94
|
const pubkeyHex = toPubkeyHex(pubkey);
|
|
97
95
|
const builderIndex = findBuilderIndexByPubkey(stateGloas, pubkey);
|
|
98
96
|
const validatorIndex = state.epochCtx.getValidatorIndex(pubkey);
|
|
99
97
|
|
|
100
|
-
// Regardless of the withdrawal credentials prefix, if a builder/validator
|
|
101
|
-
// already exists with this pubkey, apply the deposit to their balance
|
|
102
98
|
const isBuilder = builderIndex !== null;
|
|
103
99
|
const isValidator = isValidatorKnown(state, validatorIndex);
|
|
104
|
-
const isPendingValidator = pendingValidatorPubkeys.has(pubkeyHex);
|
|
105
100
|
|
|
106
|
-
if (isBuilder
|
|
107
|
-
//
|
|
101
|
+
if (isBuilder) {
|
|
102
|
+
// Top up an existing builder regardless of withdrawal credential prefix
|
|
108
103
|
applyDepositForBuilder(stateGloas, pubkey, withdrawalCredentials, amount, signature, state.slot);
|
|
109
104
|
return;
|
|
110
105
|
}
|
|
111
106
|
|
|
112
|
-
//
|
|
113
|
-
// deposit requests for the same pubkey in this envelope must see it as a pending validator
|
|
107
|
+
// Only check the (expensive) "pending validator" condition when needed
|
|
114
108
|
if (
|
|
115
|
-
|
|
109
|
+
isBuilderWithdrawalCredential(withdrawalCredentials) &&
|
|
116
110
|
!isValidator &&
|
|
117
|
-
!
|
|
118
|
-
isValidDepositSignature(state.config, pubkey, withdrawalCredentials, amount, signature)
|
|
111
|
+
!lookup.hasPendingValidator(state.config, pubkeyHex)
|
|
119
112
|
) {
|
|
120
|
-
|
|
113
|
+
applyDepositForBuilder(stateGloas, pubkey, withdrawalCredentials, amount, signature, state.slot);
|
|
114
|
+
return;
|
|
121
115
|
}
|
|
116
|
+
|
|
117
|
+
const pendingDeposit = ssz.electra.PendingDeposit.toViewDU({
|
|
118
|
+
pubkey,
|
|
119
|
+
withdrawalCredentials,
|
|
120
|
+
amount,
|
|
121
|
+
signature,
|
|
122
|
+
slot: state.slot,
|
|
123
|
+
});
|
|
124
|
+
// Keep the lookup in sync with state.pendingDeposits so later deposit-requests
|
|
125
|
+
// in the same envelope see this deposit
|
|
126
|
+
lookup.add(pendingDeposit, pubkeyHex);
|
|
127
|
+
state.pendingDeposits.push(pendingDeposit);
|
|
128
|
+
return;
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
//
|
|
125
|
-
if (
|
|
131
|
+
// Pre-Gloas (Electra) path
|
|
132
|
+
if (state.depositRequestsStartIndex === UNSET_DEPOSIT_REQUESTS_START_INDEX) {
|
|
126
133
|
state.depositRequestsStartIndex = depositRequest.index;
|
|
127
134
|
}
|
|
128
135
|
|
|
@@ -136,28 +143,3 @@ export function processDepositRequest(
|
|
|
136
143
|
});
|
|
137
144
|
state.pendingDeposits.push(pendingDeposit);
|
|
138
145
|
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Build a set of pubkeys (hex-encoded) from pending deposits that have valid signatures.
|
|
142
|
-
* This is computed once and passed to each processDepositRequest call to avoid
|
|
143
|
-
* repeatedly iterating state.pendingDeposits.
|
|
144
|
-
*
|
|
145
|
-
* Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.3/specs/gloas/beacon-chain.md#new-is_pending_validator
|
|
146
|
-
*/
|
|
147
|
-
export function getPendingValidatorPubkeys(config: BeaconConfig, state: CachedBeaconStateGloas): Set<PubkeyHex> {
|
|
148
|
-
const result = new Set<PubkeyHex>();
|
|
149
|
-
for (const pendingDeposit of state.pendingDeposits.getAllReadonly()) {
|
|
150
|
-
if (
|
|
151
|
-
isValidDepositSignature(
|
|
152
|
-
config,
|
|
153
|
-
pendingDeposit.pubkey,
|
|
154
|
-
pendingDeposit.withdrawalCredentials,
|
|
155
|
-
pendingDeposit.amount,
|
|
156
|
-
pendingDeposit.signature
|
|
157
|
-
)
|
|
158
|
-
) {
|
|
159
|
-
result.add(toPubkeyHex(pendingDeposit.pubkey));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return result;
|
|
163
|
-
}
|
|
@@ -3,8 +3,9 @@ import {BeaconBlock, electra, ssz} from "@lodestar/types";
|
|
|
3
3
|
import {byteArrayEquals, toRootHex} from "@lodestar/utils";
|
|
4
4
|
import {CachedBeaconStateGloas} from "../types.js";
|
|
5
5
|
import {computeEpochAtSlot} from "../util/epoch.js";
|
|
6
|
+
import {PendingDepositsLookup} from "../util/pendingDepositsLookup.js";
|
|
6
7
|
import {processConsolidationRequest} from "./processConsolidationRequest.js";
|
|
7
|
-
import {
|
|
8
|
+
import {processDepositRequest} from "./processDepositRequest.js";
|
|
8
9
|
import {processWithdrawalRequest} from "./processWithdrawalRequest.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -54,9 +55,9 @@ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, reque
|
|
|
54
55
|
// Process execution requests from parent's payload. The execution
|
|
55
56
|
// requests are processed at state.slot (child's slot), not the parent's slot.
|
|
56
57
|
if (requests.deposits.length > 0) {
|
|
57
|
-
const
|
|
58
|
+
const pendingDepositsLookup = PendingDepositsLookup.build(state);
|
|
58
59
|
for (const deposit of requests.deposits) {
|
|
59
|
-
processDepositRequest(fork, state, deposit,
|
|
60
|
+
processDepositRequest(fork, state, deposit, pendingDepositsLookup);
|
|
60
61
|
}
|
|
61
62
|
}
|
|
62
63
|
|
|
@@ -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
|
+
// }
|