@lodestar/state-transition 1.46.0-dev.281a8ee194 → 1.46.0-dev.3e8651f2dc
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/processDeposit.d.ts +15 -0
- package/lib/block/processDeposit.d.ts.map +1 -1
- package/lib/block/processDeposit.js +73 -1
- package/lib/block/processDeposit.js.map +1 -1
- package/lib/cache/builderDepositSignatureCache.d.ts +48 -0
- package/lib/cache/builderDepositSignatureCache.d.ts.map +1 -0
- package/lib/cache/builderDepositSignatureCache.js +70 -0
- package/lib/cache/builderDepositSignatureCache.js.map +1 -0
- package/lib/cache/epochCache.d.ts +9 -0
- package/lib/cache/epochCache.d.ts.map +1 -1
- package/lib/cache/epochCache.js +13 -0
- package/lib/cache/epochCache.js.map +1 -1
- package/lib/slot/upgradeStateToGloas.js +13 -7
- package/lib/slot/upgradeStateToGloas.js.map +1 -1
- package/lib/stateView/beaconStateView.d.ts +3 -0
- package/lib/stateView/beaconStateView.d.ts.map +1 -1
- package/lib/stateView/beaconStateView.js +7 -0
- package/lib/stateView/beaconStateView.js.map +1 -1
- package/lib/stateView/interface.d.ts +8 -0
- package/lib/stateView/interface.d.ts.map +1 -1
- package/lib/stateView/interface.js.map +1 -1
- package/lib/stateView/nativeBeaconStateView.d.ts +3 -0
- package/lib/stateView/nativeBeaconStateView.d.ts.map +1 -1
- package/lib/stateView/nativeBeaconStateView.js +6 -0
- package/lib/stateView/nativeBeaconStateView.js.map +1 -1
- package/lib/testUtils/util.d.ts +16 -1
- package/lib/testUtils/util.d.ts.map +1 -1
- package/lib/testUtils/util.js +55 -2
- package/lib/testUtils/util.js.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 +7 -10
- package/lib/util/pendingDepositsLookup.d.ts.map +1 -1
- package/lib/util/pendingDepositsLookup.js +10 -19
- package/lib/util/pendingDepositsLookup.js.map +1 -1
- package/lib/util/preVerifyBuilderDeposits.d.ts +52 -0
- package/lib/util/preVerifyBuilderDeposits.d.ts.map +1 -0
- package/lib/util/preVerifyBuilderDeposits.js +124 -0
- package/lib/util/preVerifyBuilderDeposits.js.map +1 -0
- package/package.json +7 -7
- package/src/block/processDeposit.ts +75 -1
- package/src/cache/builderDepositSignatureCache.ts +81 -0
- package/src/cache/epochCache.ts +14 -0
- package/src/slot/upgradeStateToGloas.ts +17 -10
- package/src/stateView/beaconStateView.ts +13 -0
- package/src/stateView/interface.ts +8 -0
- package/src/stateView/nativeBeaconStateView.ts +9 -0
- package/src/testUtils/util.ts +73 -3
- package/src/util/index.ts +1 -0
- package/src/util/pendingDepositsLookup.ts +13 -24
- package/src/util/preVerifyBuilderDeposits.ts +153 -0
|
@@ -172,8 +172,8 @@ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void
|
|
|
172
172
|
const pendingDeposits = ssz.gloas.PendingDeposits.defaultViewDU();
|
|
173
173
|
const pendingDepositsLookup = PendingDepositsLookup.buildEmpty();
|
|
174
174
|
|
|
175
|
-
for (
|
|
176
|
-
const
|
|
175
|
+
for (const deposit of state.pendingDeposits.getAllReadonly()) {
|
|
176
|
+
const depositValue = deposit.toValue();
|
|
177
177
|
|
|
178
178
|
const validatorIndex = state.epochCtx.getValidatorIndex(deposit.pubkey);
|
|
179
179
|
const pubkeyHex = toPubkeyHex(deposit.pubkey);
|
|
@@ -181,7 +181,7 @@ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void
|
|
|
181
181
|
// Deposits for existing validators stay in the pending queue
|
|
182
182
|
if (isValidatorKnown(state, validatorIndex)) {
|
|
183
183
|
pendingDeposits.push(deposit);
|
|
184
|
-
pendingDepositsLookup.add(
|
|
184
|
+
pendingDepositsLookup.add(depositValue, pubkeyHex);
|
|
185
185
|
continue;
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -202,26 +202,33 @@ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void
|
|
|
202
202
|
// can pick it up later.
|
|
203
203
|
if (!isBuilderWithdrawalCredential(deposit.withdrawalCredentials)) {
|
|
204
204
|
pendingDeposits.push(deposit);
|
|
205
|
-
pendingDepositsLookup.add(
|
|
205
|
+
pendingDepositsLookup.add(depositValue, pubkeyHex);
|
|
206
206
|
continue;
|
|
207
207
|
}
|
|
208
|
-
if (
|
|
208
|
+
if (
|
|
209
|
+
pendingDepositsLookup.hasPendingValidator(state.config, pubkeyHex, state.epochCtx.builderDepositSignatureCache)
|
|
210
|
+
) {
|
|
209
211
|
pendingDeposits.push(deposit);
|
|
210
|
-
pendingDepositsLookup.add(
|
|
212
|
+
pendingDepositsLookup.add(depositValue, pubkeyHex);
|
|
211
213
|
continue;
|
|
212
214
|
}
|
|
213
215
|
|
|
214
216
|
// Verify the deposit signature (proof of possession). If invalid the deposit is silently
|
|
215
217
|
// dropped — stake is forfeited, matching the validator deposit contract behavior.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
+
//
|
|
219
|
+
// The prepareNextSlot scheduler pre-verifies these signatures in the epochs before the fork
|
|
220
|
+
// A cache miss falls back to verifying this one deposit — no worse than pre-cache.
|
|
221
|
+
const cached = state.epochCtx.builderDepositSignatureCache.getSignatureValidity(depositValue);
|
|
222
|
+
const isValid =
|
|
223
|
+
cached ??
|
|
224
|
+
isValidDepositSignature(
|
|
218
225
|
state.config,
|
|
219
226
|
deposit.pubkey,
|
|
220
227
|
deposit.withdrawalCredentials,
|
|
221
228
|
deposit.amount,
|
|
222
229
|
deposit.signature
|
|
223
|
-
)
|
|
224
|
-
) {
|
|
230
|
+
);
|
|
231
|
+
if (!isValid) {
|
|
225
232
|
continue;
|
|
226
233
|
}
|
|
227
234
|
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
} from "../util/execution.js";
|
|
68
68
|
import {canBuilderCoverBid} from "../util/gloas.js";
|
|
69
69
|
import {loadState} from "../util/loadState/loadState.js";
|
|
70
|
+
import {PreVerifyBuilderDepositsResult, preVerifyBuilderDepositsPreGloas} from "../util/preVerifyBuilderDeposits.js";
|
|
70
71
|
import {getRandaoMix} from "../util/seed.js";
|
|
71
72
|
import {getLatestWeakSubjectivityCheckpointEpoch} from "../util/weakSubjectivity.js";
|
|
72
73
|
import {IBeaconStateView, IBeaconStateViewGloas, IBeaconStateViewLatestFork, isStatePostGloas} from "./interface.js";
|
|
@@ -341,6 +342,18 @@ export class BeaconStateView implements IBeaconStateViewLatestFork {
|
|
|
341
342
|
return this._proposerLookahead;
|
|
342
343
|
}
|
|
343
344
|
|
|
345
|
+
preVerifyBuilderDepositsPreGloas(maxBuilderDeposits: number, maxDurationMs: number): PreVerifyBuilderDepositsResult {
|
|
346
|
+
return preVerifyBuilderDepositsPreGloas(
|
|
347
|
+
this.cachedState as CachedBeaconStateFulu,
|
|
348
|
+
maxBuilderDeposits,
|
|
349
|
+
maxDurationMs
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
clearPreGloasBuilderDepositCache(): void {
|
|
354
|
+
this.cachedState.epochCtx.builderDepositSignatureCache.clear();
|
|
355
|
+
}
|
|
356
|
+
|
|
344
357
|
// gloas
|
|
345
358
|
|
|
346
359
|
get latestBlockHash(): Bytes32 {
|
|
@@ -52,6 +52,7 @@ import {SyncCommitteeCache} from "../cache/syncCommitteeCache.js";
|
|
|
52
52
|
import {SyncCommitteeWitness} from "../lightClient/types.js";
|
|
53
53
|
import {StateTransitionModules, StateTransitionOpts} from "../stateTransition.js";
|
|
54
54
|
import {EpochShuffling} from "../util/epochShuffling.js";
|
|
55
|
+
import {PreVerifyBuilderDepositsResult} from "../util/preVerifyBuilderDeposits.js";
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* A read-only view of the BeaconState.
|
|
@@ -242,6 +243,13 @@ export interface IBeaconStateViewElectra extends IBeaconStateViewDeneb {
|
|
|
242
243
|
export interface IBeaconStateViewFulu extends IBeaconStateViewElectra {
|
|
243
244
|
forkName: ForkPostFulu;
|
|
244
245
|
proposerLookahead: fulu.ProposerLookahead;
|
|
246
|
+
/**
|
|
247
|
+
* Pre-verify a slice of builder-prefix pending deposits and cache the results on the underlying
|
|
248
|
+
* `BuilderDepositSignatureCache` (Fulu is the fork immediately before Gloas).
|
|
249
|
+
*/
|
|
250
|
+
preVerifyBuilderDepositsPreGloas(maxBuilderDeposits: number, maxDurationMs: number): PreVerifyBuilderDepositsResult;
|
|
251
|
+
/** Drop the pre-Gloas builder-deposit signature cache (called once the Gloas fork is finalized). */
|
|
252
|
+
clearPreGloasBuilderDepositCache(): void;
|
|
245
253
|
}
|
|
246
254
|
|
|
247
255
|
/** Gloas+ state fields — use isStatePostGloas() guard */
|
|
@@ -35,6 +35,7 @@ import {SyncCommitteeCache} from "../cache/syncCommitteeCache.js";
|
|
|
35
35
|
import {SyncCommitteeWitness} from "../lightClient/types.js";
|
|
36
36
|
import {StateTransitionModules, StateTransitionOpts} from "../stateTransition.js";
|
|
37
37
|
import {EpochShuffling} from "../util/epochShuffling.js";
|
|
38
|
+
import {PreVerifyBuilderDepositsResult} from "../util/preVerifyBuilderDeposits.js";
|
|
38
39
|
import {
|
|
39
40
|
IBeaconStateView,
|
|
40
41
|
IBeaconStateViewGloas,
|
|
@@ -845,6 +846,14 @@ export class NativeBeaconStateView implements IBeaconStateViewLatestFork {
|
|
|
845
846
|
return this._proposerLookahead;
|
|
846
847
|
}
|
|
847
848
|
|
|
849
|
+
preVerifyBuilderDepositsPreGloas(maxBuilderDeposits: number, maxDurationMs: number): PreVerifyBuilderDepositsResult {
|
|
850
|
+
return this.binding.preVerifyBuilderDepositsPreGloas(maxBuilderDeposits, maxDurationMs);
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
clearPreGloasBuilderDepositCache(): void {
|
|
854
|
+
this.binding.clearPreGloasBuilderDepositCache();
|
|
855
|
+
}
|
|
856
|
+
|
|
848
857
|
// ─── gloas ───────────────────────────────────────────────────────────────
|
|
849
858
|
|
|
850
859
|
get latestBlockHash(): Bytes32 {
|
package/src/testUtils/util.ts
CHANGED
|
@@ -1,25 +1,28 @@
|
|
|
1
1
|
import {PublicKey, SecretKey} from "@chainsafe/blst";
|
|
2
2
|
import {BitArray, fromHexString} from "@chainsafe/ssz";
|
|
3
|
-
import {createBeaconConfig, createChainForkConfig} from "@lodestar/config";
|
|
3
|
+
import {BeaconConfig, createBeaconConfig, createChainForkConfig} from "@lodestar/config";
|
|
4
4
|
import {config} from "@lodestar/config/default";
|
|
5
5
|
import {
|
|
6
|
+
BUILDER_WITHDRAWAL_PREFIX,
|
|
7
|
+
DOMAIN_DEPOSIT,
|
|
6
8
|
EPOCHS_PER_ETH1_VOTING_PERIOD,
|
|
7
9
|
EPOCHS_PER_HISTORICAL_VECTOR,
|
|
8
10
|
ForkName,
|
|
9
11
|
ForkSeq,
|
|
12
|
+
GENESIS_SLOT,
|
|
10
13
|
MAX_ATTESTATIONS,
|
|
11
14
|
MAX_EFFECTIVE_BALANCE,
|
|
12
15
|
SLOTS_PER_EPOCH,
|
|
13
16
|
SLOTS_PER_HISTORICAL_ROOT,
|
|
14
17
|
} from "@lodestar/params";
|
|
15
|
-
import {BeaconState, Slot, phase0, ssz} from "@lodestar/types";
|
|
18
|
+
import {BeaconState, Slot, electra, phase0, ssz} from "@lodestar/types";
|
|
16
19
|
import {getEffectiveBalanceIncrements} from "../cache/effectiveBalanceIncrements.js";
|
|
20
|
+
import {ZERO_HASH} from "../constants/index.js";
|
|
17
21
|
import {
|
|
18
22
|
computeCommitteeCount,
|
|
19
23
|
computeEpochAtSlot,
|
|
20
24
|
createCachedBeaconState,
|
|
21
25
|
createPubkeyCache,
|
|
22
|
-
interopSecretKey,
|
|
23
26
|
newFilledArray,
|
|
24
27
|
processSlots,
|
|
25
28
|
} from "../index.js";
|
|
@@ -32,6 +35,9 @@ import {
|
|
|
32
35
|
CachedBeaconStateElectra,
|
|
33
36
|
CachedBeaconStatePhase0,
|
|
34
37
|
} from "../types.js";
|
|
38
|
+
import {computeDomain} from "../util/domain.js";
|
|
39
|
+
import {interopSecretKey} from "../util/interop.js";
|
|
40
|
+
import {computeSigningRoot} from "../util/signingRoot.js";
|
|
35
41
|
import {getNextSyncCommittee} from "../util/syncCommittee.js";
|
|
36
42
|
import {getActiveValidatorIndices} from "../util/validator.js";
|
|
37
43
|
import {interopPubkeysCached} from "./interop.js";
|
|
@@ -541,3 +547,67 @@ export function generateTestCachedBeaconStateOnlyValidators({
|
|
|
541
547
|
{skipSyncPubkeys: true}
|
|
542
548
|
);
|
|
543
549
|
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Generate `count` validly-signed builder pending deposits with distinct interop pubkeys
|
|
553
|
+
* [startIndex, startIndex + count). Withdrawal credentials carry the BUILDER_WITHDRAWAL_PREFIX so
|
|
554
|
+
* `isBuilderWithdrawalCredential` recognizes them. Used to exercise the pre-Gloas builder-deposit
|
|
555
|
+
* pre-verify scanner and the fork-transition onboarding path.
|
|
556
|
+
*/
|
|
557
|
+
export function generateBuilderPendingDeposits(
|
|
558
|
+
config: BeaconConfig,
|
|
559
|
+
count: number,
|
|
560
|
+
startIndex: number
|
|
561
|
+
): electra.PendingDeposit[] {
|
|
562
|
+
// Deposits use a fork-agnostic domain, see `isValidDepositSignature`
|
|
563
|
+
const domain = computeDomain(DOMAIN_DEPOSIT, config.GENESIS_FORK_VERSION, ZERO_HASH);
|
|
564
|
+
const amount = 1_000_000_000; // 1 ETH in Gwei
|
|
565
|
+
const deposits: electra.PendingDeposit[] = [];
|
|
566
|
+
|
|
567
|
+
for (let i = 0; i < count; i++) {
|
|
568
|
+
const sk = interopSecretKey(startIndex + i);
|
|
569
|
+
const pubkey = sk.toPublicKey().toBytes();
|
|
570
|
+
|
|
571
|
+
const withdrawalCredentials = Buffer.alloc(32, 0);
|
|
572
|
+
withdrawalCredentials[0] = BUILDER_WITHDRAWAL_PREFIX;
|
|
573
|
+
// bytes [12, 32) hold the 20-byte builder execution address
|
|
574
|
+
withdrawalCredentials.fill((startIndex + i) & 0xff, 12, 32);
|
|
575
|
+
|
|
576
|
+
const signingRoot = computeSigningRoot(ssz.phase0.DepositMessage, {pubkey, withdrawalCredentials, amount}, domain);
|
|
577
|
+
|
|
578
|
+
deposits.push({
|
|
579
|
+
pubkey,
|
|
580
|
+
withdrawalCredentials,
|
|
581
|
+
amount,
|
|
582
|
+
signature: sk.sign(signingRoot).toBytes(),
|
|
583
|
+
slot: GENESIS_SLOT,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return deposits;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Generate a validly-signed *validator* (0x00-prefix) pending deposit for interop key `keyIndex`.
|
|
592
|
+
* Shares its pubkey with `generateBuilderPendingDeposits(config, 1, keyIndex)`, so it stands in for a
|
|
593
|
+
* validator deposit competing with a builder deposit for the same pubkey (the `hasPendingValidator`
|
|
594
|
+
* path). `amount` lets callers build otherwise-identical-but-distinct deposit value objects.
|
|
595
|
+
*/
|
|
596
|
+
export function generateValidatorPendingDeposit(
|
|
597
|
+
config: BeaconConfig,
|
|
598
|
+
keyIndex: number,
|
|
599
|
+
amount = 32_000_000_000
|
|
600
|
+
): electra.PendingDeposit {
|
|
601
|
+
const domain = computeDomain(DOMAIN_DEPOSIT, config.GENESIS_FORK_VERSION, ZERO_HASH);
|
|
602
|
+
const sk = interopSecretKey(keyIndex);
|
|
603
|
+
const pubkey = sk.toPublicKey().toBytes();
|
|
604
|
+
const withdrawalCredentials = Buffer.alloc(32, 0); // 0x00 prefix → validator, not a builder
|
|
605
|
+
const signingRoot = computeSigningRoot(ssz.phase0.DepositMessage, {pubkey, withdrawalCredentials, amount}, domain);
|
|
606
|
+
return {
|
|
607
|
+
pubkey,
|
|
608
|
+
withdrawalCredentials,
|
|
609
|
+
amount,
|
|
610
|
+
signature: sk.sign(signingRoot).toBytes(),
|
|
611
|
+
slot: GENESIS_SLOT,
|
|
612
|
+
};
|
|
613
|
+
}
|
package/src/util/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export * from "./gloas.js";
|
|
|
20
20
|
export * from "./interop.js";
|
|
21
21
|
export * from "./loadState/index.js";
|
|
22
22
|
export * from "./pendingDepositsLookup.js";
|
|
23
|
+
export * from "./preVerifyBuilderDeposits.js";
|
|
23
24
|
export * from "./rootCache.js";
|
|
24
25
|
export * from "./seed.js";
|
|
25
26
|
export * from "./shuffling.js";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import {BeaconConfig} from "@lodestar/config";
|
|
2
2
|
import {PubkeyHex, electra} from "@lodestar/types";
|
|
3
|
-
import {toPubkeyHex} from "@lodestar/utils";
|
|
4
3
|
import {isValidDepositSignature} from "../block/processDeposit.js";
|
|
5
|
-
import {
|
|
4
|
+
import {BuilderDepositSignatureCache} from "../cache/builderDepositSignatureCache.js";
|
|
6
5
|
|
|
7
6
|
type PendingDepositsValidation = {
|
|
8
7
|
hasValidSignature: boolean;
|
|
@@ -34,28 +33,14 @@ export class PendingDepositsLookup {
|
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
|
-
*
|
|
38
|
-
* No BLS work is done here; signature verification happens lazily in `hasPendingValidator`.
|
|
36
|
+
* Append a pending deposit to the represented sequence, keyed by its (precomputed) `pubkeyHex`.
|
|
39
37
|
*/
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
for (const pendingDeposit of state.pendingDeposits.getAllReadonly()) {
|
|
43
|
-
lookup.add(pendingDeposit);
|
|
44
|
-
}
|
|
45
|
-
return lookup;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Append a pending deposit to the represented sequence.
|
|
50
|
-
* Pass `pubkeyHex` if the caller has already computed it.
|
|
51
|
-
*/
|
|
52
|
-
add(pendingDeposit: electra.PendingDeposit, pubkeyHex?: PubkeyHex): void {
|
|
53
|
-
const key = pubkeyHex ?? toPubkeyHex(pendingDeposit.pubkey);
|
|
54
|
-
const existing = this.depositsByPubkey.get(key);
|
|
38
|
+
add(pendingDeposit: electra.PendingDeposit, pubkeyHex: PubkeyHex): void {
|
|
39
|
+
const existing = this.depositsByPubkey.get(pubkeyHex);
|
|
55
40
|
if (existing) {
|
|
56
41
|
existing.push(pendingDeposit);
|
|
57
42
|
} else {
|
|
58
|
-
this.depositsByPubkey.set(
|
|
43
|
+
this.depositsByPubkey.set(pubkeyHex, [pendingDeposit]);
|
|
59
44
|
}
|
|
60
45
|
}
|
|
61
46
|
|
|
@@ -63,8 +48,11 @@ export class PendingDepositsLookup {
|
|
|
63
48
|
* Returns true if any pending deposit for `pubkeyHex` has a valid BLS deposit signature.
|
|
64
49
|
* Memoizes the result in `validationCache` so repeated checks for the same pubkey
|
|
65
50
|
* within a block only verify deposits that have not already been checked.
|
|
51
|
+
*
|
|
52
|
+
* Most of the time it should hit the BuilderDepositSignatureCache so we don't have to validate
|
|
53
|
+
* signatures here.
|
|
66
54
|
*/
|
|
67
|
-
hasPendingValidator(config: BeaconConfig, pubkeyHex: PubkeyHex): boolean {
|
|
55
|
+
hasPendingValidator(config: BeaconConfig, pubkeyHex: PubkeyHex, cache: BuilderDepositSignatureCache): boolean {
|
|
68
56
|
const validation = this.validationCache.get(pubkeyHex);
|
|
69
57
|
if (validation?.hasValidSignature === true) {
|
|
70
58
|
return true;
|
|
@@ -85,15 +73,16 @@ export class PendingDepositsLookup {
|
|
|
85
73
|
|
|
86
74
|
for (let i = startIndex; i < deposits.length; i++) {
|
|
87
75
|
const deposit = deposits[i];
|
|
88
|
-
|
|
76
|
+
const isValid =
|
|
77
|
+
cache.getSignatureValidity(deposit) ??
|
|
89
78
|
isValidDepositSignature(
|
|
90
79
|
config,
|
|
91
80
|
deposit.pubkey,
|
|
92
81
|
deposit.withdrawalCredentials,
|
|
93
82
|
deposit.amount,
|
|
94
83
|
deposit.signature
|
|
95
|
-
)
|
|
96
|
-
) {
|
|
84
|
+
);
|
|
85
|
+
if (isValid) {
|
|
97
86
|
this.validationCache.set(pubkeyHex, {hasValidSignature: true, validatedCount: i + 1});
|
|
98
87
|
return true;
|
|
99
88
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {electra} from "@lodestar/types";
|
|
2
|
+
import {toPubkeyHex} from "@lodestar/utils";
|
|
3
|
+
import {verifyDepositSignatures} from "../block/processDeposit.js";
|
|
4
|
+
import {CachedBeaconStateFulu} from "../types.js";
|
|
5
|
+
import {isBuilderWithdrawalCredential} from "./gloas.js";
|
|
6
|
+
|
|
7
|
+
/** Verify queued deposit signatures in batches of this size. Also the time-box granularity: the
|
|
8
|
+
* deadline is checked once per batch, so overrun is bounded to a single batch. Exported for tests. */
|
|
9
|
+
export const BUILDER_DEPOSIT_BATCH_SIZE = 32;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Per-tick cap on builder deposits the prepareForNextSlot scanner will verify.
|
|
13
|
+
* ~2.7s for 10_000 BLS verifications on a typical server — fits within the slot budget while the
|
|
14
|
+
* pre-window (see GLOAS_PREVERIFY_WINDOW_EPOCHS) covers up to ~620k deposits over its duration.
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_BUILDER_DEPOSITS_PER_SLOT = 10_000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Number of epochs before GLOAS_FORK_EPOCH during which the prepareForNextSlot scanner pre-verifies
|
|
20
|
+
* builder-deposit signatures. Two: deposits arriving this late cannot collude with validator
|
|
21
|
+
* deposits — validator-vs-builder routing is decided at the fork boundary from what is already in
|
|
22
|
+
* pendingDeposits, so anything submitted inside this window is unambiguous.
|
|
23
|
+
*/
|
|
24
|
+
export const GLOAS_PREVERIFY_WINDOW_EPOCHS = 2;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Summary of a single `preVerifyBuilderDepositsPreGloas` call.
|
|
28
|
+
*/
|
|
29
|
+
export type PreVerifyBuilderDepositsResult = {
|
|
30
|
+
/** Builder-prefix deposit signatures verified valid in this call (cached true). */
|
|
31
|
+
validBuilderSignaturesCount: number;
|
|
32
|
+
/** Builder-prefix deposit signatures verified invalid in this call (cached false, dropped at fork). */
|
|
33
|
+
invalidBuilderSignaturesCount: number;
|
|
34
|
+
/** Validator (builder-pubkey) deposit signatures verified valid in this call. */
|
|
35
|
+
validValidatorSignaturesCount: number;
|
|
36
|
+
/** Validator (builder-pubkey) deposit signatures verified invalid in this call (abuse signal). */
|
|
37
|
+
invalidValidatorSignaturesCount: number;
|
|
38
|
+
/** Pending deposits examined this call (all creds; < pendingDepositsCount ⇒ per-tick cap hit). */
|
|
39
|
+
scannedPendingDeposits: number;
|
|
40
|
+
/** Cumulative deposits verified & cached so far this window (cache size, pass + fail). */
|
|
41
|
+
totalCachedDeposits: number;
|
|
42
|
+
/** Distinct builder pubkeys tracked so far this window. */
|
|
43
|
+
totalBuilderPubkeys: number;
|
|
44
|
+
/** Current `state.pendingDeposits.length` — the full backlog. */
|
|
45
|
+
pendingDepositsCount: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Scanner driven by `prepareForNextSlot` over the `GLOAS_PREVERIFY_WINDOW_EPOCHS` epochs before
|
|
50
|
+
* GLOAS_FORK_EPOCH. Walks `state.pendingDeposits` (as struct values via getAllReadonlyValues) and
|
|
51
|
+
* queues, for signature verification, the deposits the fork transition will check on its hot path:
|
|
52
|
+
* - builder-prefix deposits (for onboarding); records each builder pubkey on the cache, and
|
|
53
|
+
* - validator deposits sharing a builder pubkey (the set `hasPendingValidator` walks at the fork),
|
|
54
|
+
* but only until one is found valid — `hasPendingValidator` stops at the first valid signature, so
|
|
55
|
+
* the cache marks the pubkey and the scanner skips that pubkey's remaining validator deposits.
|
|
56
|
+
*
|
|
57
|
+
* Bounded by two knobs: at most `maxBuilderDeposits` new deposits are queued per call, and the chunk
|
|
58
|
+
* verification is **time-boxed** to `maxDurationMs`.
|
|
59
|
+
*/
|
|
60
|
+
export function preVerifyBuilderDepositsPreGloas(
|
|
61
|
+
state: CachedBeaconStateFulu,
|
|
62
|
+
maxBuilderDeposits: number,
|
|
63
|
+
maxDurationMs: number
|
|
64
|
+
): PreVerifyBuilderDepositsResult {
|
|
65
|
+
const cache = state.epochCtx.builderDepositSignatureCache;
|
|
66
|
+
|
|
67
|
+
// Collect uncached deposits to verify as struct values. Full rescan each tick: iteration is
|
|
68
|
+
// O(pendingDeposits) but runs in prepareNextSlot spare time, and isVerified() is an O(1) skip for
|
|
69
|
+
// already-cached deposits. For a ContainerNodeStructType, getAllReadonlyValues() returns each
|
|
70
|
+
// node.value by reference — the stable cache key.
|
|
71
|
+
const queue: electra.PendingDeposit[] = [];
|
|
72
|
+
let scannedPendingDeposits = 0;
|
|
73
|
+
for (const deposit of state.pendingDeposits.getAllReadonlyValues()) {
|
|
74
|
+
if (queue.length >= maxBuilderDeposits) break; // per-tick cap; next tick resumes via isVerified()
|
|
75
|
+
scannedPendingDeposits++;
|
|
76
|
+
if (cache.isVerified(deposit)) continue; // already verified in an earlier tick
|
|
77
|
+
|
|
78
|
+
if (isBuilderWithdrawalCredential(deposit.withdrawalCredentials)) {
|
|
79
|
+
// Record the builder pubkey so validator deposits sharing it get pre-verified — this or a
|
|
80
|
+
// later tick, since those validator deposits precede the builder deposit in the queue. A cached
|
|
81
|
+
// builder deposit already added its pubkey, so it's safe to skip via the `continue` above.
|
|
82
|
+
cache.addBuilderPubkey(toPubkeyHex(deposit.pubkey));
|
|
83
|
+
queue.push(deposit);
|
|
84
|
+
} else {
|
|
85
|
+
// Validator deposit — pre-verify only if it shares a builder pubkey (the set hasPendingValidator
|
|
86
|
+
// walks at the fork) AND we haven't already cached a valid validator deposit for that pubkey.
|
|
87
|
+
// this is the same logic to hasPendingValidator.
|
|
88
|
+
const pubkeyHex = toPubkeyHex(deposit.pubkey);
|
|
89
|
+
if (cache.isBuilderPubkey(pubkeyHex) && !cache.hasValidValidatorDeposit(pubkeyHex)) {
|
|
90
|
+
queue.push(deposit);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const pendingDepositsCount = state.pendingDeposits.length;
|
|
96
|
+
if (queue.length === 0) {
|
|
97
|
+
return {
|
|
98
|
+
validBuilderSignaturesCount: 0,
|
|
99
|
+
invalidBuilderSignaturesCount: 0,
|
|
100
|
+
validValidatorSignaturesCount: 0,
|
|
101
|
+
invalidValidatorSignaturesCount: 0,
|
|
102
|
+
scannedPendingDeposits,
|
|
103
|
+
totalCachedDeposits: cache.cachedDepositCount,
|
|
104
|
+
totalBuilderPubkeys: cache.builderPubkeyCount,
|
|
105
|
+
pendingDepositsCount,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Verify in chunks; chunking caps the fallback blast radius so one bad signature only forces 32
|
|
110
|
+
// individual re-verifications. Key each result by the deposit value object. Counts are tracked
|
|
111
|
+
// incrementally (not queue.length - verified) so they stay correct when we time-box out early.
|
|
112
|
+
const deadline = Date.now() + maxDurationMs;
|
|
113
|
+
let validBuilderSignaturesCount = 0;
|
|
114
|
+
let invalidBuilderSignaturesCount = 0;
|
|
115
|
+
let validValidatorSignaturesCount = 0;
|
|
116
|
+
let invalidValidatorSignaturesCount = 0;
|
|
117
|
+
for (let chunkStart = 0; chunkStart < queue.length; chunkStart += BUILDER_DEPOSIT_BATCH_SIZE) {
|
|
118
|
+
const end = Math.min(chunkStart + BUILDER_DEPOSIT_BATCH_SIZE, queue.length);
|
|
119
|
+
const chunk = queue.slice(chunkStart, end);
|
|
120
|
+
const chunkResults = verifyDepositSignatures(state.epochCtx.config, chunk);
|
|
121
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
122
|
+
cache.setSignatureValidity(chunk[j], chunkResults[j]);
|
|
123
|
+
const isBuilder = isBuilderWithdrawalCredential(chunk[j].withdrawalCredentials);
|
|
124
|
+
if (chunkResults[j]) {
|
|
125
|
+
if (isBuilder) {
|
|
126
|
+
validBuilderSignaturesCount++;
|
|
127
|
+
} else {
|
|
128
|
+
validValidatorSignaturesCount++;
|
|
129
|
+
// A valid validator deposit satisfies hasPendingValidator() for this pubkey; stop
|
|
130
|
+
// pre-verifying its remaining validator deposits on later ticks.
|
|
131
|
+
cache.setValidValidatorDeposit(toPubkeyHex(chunk[j].pubkey));
|
|
132
|
+
}
|
|
133
|
+
} else if (isBuilder) {
|
|
134
|
+
invalidBuilderSignaturesCount++;
|
|
135
|
+
} else {
|
|
136
|
+
invalidValidatorSignaturesCount++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Time-box: keep everything verified so far (already cached) and stop
|
|
140
|
+
if (Date.now() >= deadline) break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
validBuilderSignaturesCount,
|
|
145
|
+
invalidBuilderSignaturesCount,
|
|
146
|
+
validValidatorSignaturesCount,
|
|
147
|
+
invalidValidatorSignaturesCount,
|
|
148
|
+
scannedPendingDeposits,
|
|
149
|
+
totalCachedDeposits: cache.cachedDepositCount,
|
|
150
|
+
totalBuilderPubkeys: cache.builderPubkeyCount,
|
|
151
|
+
pendingDepositsCount,
|
|
152
|
+
};
|
|
153
|
+
}
|