@lodestar/beacon-node 1.50.0-dev.335476095e → 1.50.0-dev.34984af797
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/api/impl/beacon/state/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/state/index.js +28 -6
- package/lib/api/impl/beacon/state/index.js.map +1 -1
- package/lib/api/impl/debug/index.d.ts.map +1 -1
- package/lib/api/impl/debug/index.js +26 -18
- package/lib/api/impl/debug/index.js.map +1 -1
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +6 -0
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/archiveStore/archiveStore.d.ts +5 -0
- package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
- package/lib/chain/archiveStore/archiveStore.js +13 -23
- package/lib/chain/archiveStore/archiveStore.js.map +1 -1
- package/lib/chain/archiveStore/utils/pruneHistory.d.ts +1 -1
- package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
- package/lib/chain/archiveStore/utils/pruneHistory.js +2 -1
- package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
- package/lib/chain/blocks/importBlock.d.ts.map +1 -1
- package/lib/chain/blocks/importBlock.js +13 -6
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
- package/lib/chain/blocks/verifyBlock.js +6 -6
- package/lib/chain/blocks/verifyBlock.js.map +1 -1
- package/lib/chain/chain.d.ts +3 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +47 -20
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/interface.d.ts +1 -0
- package/lib/chain/interface.d.ts.map +1 -1
- package/lib/chain/opPools/aggregatedAttestationPool.d.ts +4 -2
- package/lib/chain/opPools/aggregatedAttestationPool.d.ts.map +1 -1
- package/lib/chain/opPools/aggregatedAttestationPool.js +30 -8
- package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
- package/lib/chain/options.d.ts +1 -0
- package/lib/chain/options.d.ts.map +1 -1
- package/lib/chain/seenCache/seenGossipBlockInput.d.ts +7 -0
- package/lib/chain/seenCache/seenGossipBlockInput.d.ts.map +1 -1
- package/lib/chain/seenCache/seenGossipBlockInput.js +21 -0
- package/lib/chain/seenCache/seenGossipBlockInput.js.map +1 -1
- package/lib/chain/shufflingCache.d.ts +18 -8
- package/lib/chain/shufflingCache.d.ts.map +1 -1
- package/lib/chain/shufflingCache.js +72 -18
- package/lib/chain/shufflingCache.js.map +1 -1
- package/lib/db/beacon.d.ts +2 -0
- package/lib/db/beacon.d.ts.map +1 -1
- package/lib/db/beacon.js +3 -0
- package/lib/db/beacon.js.map +1 -1
- package/lib/db/buckets.d.ts +2 -1
- package/lib/db/buckets.d.ts.map +1 -1
- package/lib/db/buckets.js +1 -0
- package/lib/db/buckets.js.map +1 -1
- package/lib/db/interface.d.ts +2 -0
- package/lib/db/interface.d.ts.map +1 -1
- package/lib/db/repositories/blockArchiveIndex.d.ts +2 -2
- package/lib/db/repositories/blockArchiveIndex.d.ts.map +1 -1
- package/lib/db/repositories/earliestAvailableSlot.d.ts +11 -0
- package/lib/db/repositories/earliestAvailableSlot.d.ts.map +1 -0
- package/lib/db/repositories/earliestAvailableSlot.js +19 -0
- package/lib/db/repositories/earliestAvailableSlot.js.map +1 -0
- package/lib/metrics/metrics/lodestar.d.ts +7 -0
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +17 -0
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +59 -63
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/node/nodejs.d.ts +3 -1
- package/lib/node/nodejs.d.ts.map +1 -1
- package/lib/node/nodejs.js +2 -1
- package/lib/node/nodejs.js.map +1 -1
- package/lib/sync/range/batch.d.ts +5 -5
- package/lib/sync/range/batch.d.ts.map +1 -1
- package/lib/sync/range/batch.js +11 -17
- package/lib/sync/range/batch.js.map +1 -1
- package/lib/sync/range/chain.d.ts +4 -4
- package/lib/sync/range/chain.d.ts.map +1 -1
- package/lib/sync/range/chain.js +7 -5
- package/lib/sync/range/chain.js.map +1 -1
- package/lib/sync/range/range.d.ts.map +1 -1
- package/lib/sync/range/range.js +25 -11
- package/lib/sync/range/range.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts +5 -2
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +44 -20
- package/lib/sync/unknownBlock.js.map +1 -1
- package/lib/sync/utils/downloadByRange.d.ts +38 -14
- package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
- package/lib/sync/utils/downloadByRange.js +84 -39
- package/lib/sync/utils/downloadByRange.js.map +1 -1
- package/package.json +16 -16
- package/src/api/impl/beacon/state/index.ts +43 -6
- package/src/api/impl/debug/index.ts +28 -19
- package/src/api/impl/validator/index.ts +8 -0
- package/src/chain/archiveStore/archiveStore.ts +18 -24
- package/src/chain/archiveStore/utils/pruneHistory.ts +4 -1
- package/src/chain/blocks/importBlock.ts +16 -5
- package/src/chain/blocks/verifyBlock.ts +7 -6
- package/src/chain/chain.ts +68 -19
- package/src/chain/interface.ts +1 -0
- package/src/chain/opPools/aggregatedAttestationPool.ts +34 -10
- package/src/chain/options.ts +1 -1
- package/src/chain/seenCache/seenGossipBlockInput.ts +23 -0
- package/src/chain/shufflingCache.ts +84 -22
- package/src/db/beacon.ts +3 -0
- package/src/db/buckets.ts +1 -0
- package/src/db/interface.ts +2 -0
- package/src/db/repositories/earliestAvailableSlot.ts +19 -0
- package/src/metrics/metrics/lodestar.ts +18 -0
- package/src/network/processor/gossipHandlers.ts +90 -76
- package/src/node/nodejs.ts +4 -0
- package/src/sync/range/batch.ts +14 -19
- package/src/sync/range/chain.ts +9 -7
- package/src/sync/range/range.ts +26 -12
- package/src/sync/unknownBlock.ts +57 -21
- package/src/sync/utils/downloadByRange.ts +123 -58
|
@@ -7,14 +7,12 @@ import {
|
|
|
7
7
|
getIndexedAttestation,
|
|
8
8
|
} from "@lodestar/state-transition";
|
|
9
9
|
import {Attestation, CommitteeIndex, Epoch, IndexedAttestation, RootHex, Slot} from "@lodestar/types";
|
|
10
|
-
import {LodestarError, Logger, MapDef
|
|
10
|
+
import {LodestarError, Logger, MapDef} from "@lodestar/utils";
|
|
11
11
|
import {Metrics} from "../metrics/metrics.js";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* - don't have shuffling to verify attestations, need to do 1 epoch transition to add shuffling to this cache. This never happens
|
|
17
|
-
* with default chain option of maxSkipSlots = 32
|
|
14
|
+
* Lookahead duties, gossip validation, and block production need epochs n-1, n, and n+1 for current epoch n.
|
|
15
|
+
* Keep the 4 highest epochs so that it works for epoch rotation.
|
|
18
16
|
**/
|
|
19
17
|
const MAX_EPOCHS = 4;
|
|
20
18
|
|
|
@@ -37,24 +35,29 @@ type ShufflingCacheItem = {
|
|
|
37
35
|
type PromiseCacheItem = {
|
|
38
36
|
type: CacheItemType.promise;
|
|
39
37
|
timeInsertedMs: number;
|
|
40
|
-
promise
|
|
41
|
-
|
|
38
|
+
/** Resolves to null if the promise is cancelled */
|
|
39
|
+
promise: Promise<EpochShuffling | null>;
|
|
40
|
+
resolveFn: (shuffling: EpochShuffling | null) => void;
|
|
42
41
|
};
|
|
43
42
|
|
|
44
43
|
type CacheItem = ShufflingCacheItem | PromiseCacheItem;
|
|
45
44
|
|
|
45
|
+
export enum ShufflingPromiseCancelReason {
|
|
46
|
+
regenError = "regen_error",
|
|
47
|
+
decisionRootMismatch = "decision_root_mismatch",
|
|
48
|
+
pruned = "pruned",
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
export type ShufflingCacheOpts = {
|
|
47
52
|
maxShufflingCacheEpochs?: number;
|
|
48
53
|
};
|
|
49
54
|
|
|
50
55
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* - if a shuffling is not available (which does not happen with default chain option of maxSkipSlots = 32), track a promise to make sure we don't compute the same shuffling twice
|
|
54
|
-
* - skip computing shuffling when loading state bytes from disk
|
|
56
|
+
* Cache recent epoch shufflings for lookahead duties, gossip validation, and block production.
|
|
57
|
+
* Pending promises deduplicate computation of missing shufflings.
|
|
55
58
|
*/
|
|
56
59
|
export class ShufflingCache {
|
|
57
|
-
/**
|
|
60
|
+
/** Pruned to the `maxEpochs` highest epochs every time we add a shuffling */
|
|
58
61
|
private readonly itemsByDecisionRootByEpoch: MapDef<Epoch, Map<RootHex, CacheItem>> = new MapDef(
|
|
59
62
|
() => new Map<RootHex, CacheItem>()
|
|
60
63
|
);
|
|
@@ -73,6 +76,9 @@ export class ShufflingCache {
|
|
|
73
76
|
Array.from(this.itemsByDecisionRootByEpoch.values()).reduce((total, innerMap) => total + innerMap.size, 0)
|
|
74
77
|
)
|
|
75
78
|
);
|
|
79
|
+
metrics.shufflingCache.epochs.addCollect(() =>
|
|
80
|
+
metrics.shufflingCache.epochs.set(this.itemsByDecisionRootByEpoch.size)
|
|
81
|
+
);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
this.maxEpochs = opts.maxShufflingCacheEpochs ?? MAX_EPOCHS;
|
|
@@ -97,8 +103,8 @@ export class ShufflingCache {
|
|
|
97
103
|
`Too many shuffling promises: ${promiseCount}, shufflingEpoch: ${epoch}, decisionRootHex: ${decisionRoot}`
|
|
98
104
|
);
|
|
99
105
|
}
|
|
100
|
-
let resolveFn: ((shuffling: EpochShuffling) => void) | null = null;
|
|
101
|
-
const promise = new Promise<EpochShuffling>((resolve) => {
|
|
106
|
+
let resolveFn: ((shuffling: EpochShuffling | null) => void) | null = null;
|
|
107
|
+
const promise = new Promise<EpochShuffling | null>((resolve) => {
|
|
102
108
|
resolveFn = resolve;
|
|
103
109
|
});
|
|
104
110
|
if (resolveFn === null) {
|
|
@@ -121,7 +127,7 @@ export class ShufflingCache {
|
|
|
121
127
|
* Return null if we don't have a shuffling for this epoch and dependentRootHex.
|
|
122
128
|
*/
|
|
123
129
|
async get(epoch: Epoch, decisionRoot: RootHex): Promise<EpochShuffling | null> {
|
|
124
|
-
const cacheItem = this.itemsByDecisionRootByEpoch.
|
|
130
|
+
const cacheItem = this.itemsByDecisionRootByEpoch.get(epoch)?.get(decisionRoot);
|
|
125
131
|
if (cacheItem === undefined) {
|
|
126
132
|
this.metrics?.shufflingCache.miss.inc();
|
|
127
133
|
return null;
|
|
@@ -136,12 +142,10 @@ export class ShufflingCache {
|
|
|
136
142
|
}
|
|
137
143
|
|
|
138
144
|
/**
|
|
139
|
-
* Get a shuffling synchronously, return null if not present.
|
|
140
|
-
* The only time we have a promise cache item is when we regen shuffling for attestation, which never happens
|
|
141
|
-
* with default chain option.
|
|
145
|
+
* Get a shuffling synchronously, return null if not present or if it's still being computed.
|
|
142
146
|
*/
|
|
143
147
|
getSync(epoch: Epoch, decisionRoot: RootHex): EpochShuffling | null {
|
|
144
|
-
const cacheItem = this.itemsByDecisionRootByEpoch.
|
|
148
|
+
const cacheItem = this.itemsByDecisionRootByEpoch.get(epoch)?.get(decisionRoot);
|
|
145
149
|
if (cacheItem === undefined) {
|
|
146
150
|
this.metrics?.shufflingCache.miss.inc();
|
|
147
151
|
return null;
|
|
@@ -161,7 +165,7 @@ export class ShufflingCache {
|
|
|
161
165
|
* `NativeBeaconStateView`.
|
|
162
166
|
*/
|
|
163
167
|
has(epoch: Epoch, decisionRoot: RootHex): boolean {
|
|
164
|
-
const cacheItem = this.itemsByDecisionRootByEpoch.
|
|
168
|
+
const cacheItem = this.itemsByDecisionRootByEpoch.get(epoch)?.get(decisionRoot);
|
|
165
169
|
return cacheItem !== undefined && isShufflingCacheItem(cacheItem);
|
|
166
170
|
}
|
|
167
171
|
|
|
@@ -186,6 +190,25 @@ export class ShufflingCache {
|
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Resolve a pending promise with null so that waiters don't hang, then remove it.
|
|
195
|
+
*/
|
|
196
|
+
cancelPromise(epoch: Epoch, decisionRoot: RootHex, reason: ShufflingPromiseCancelReason): void {
|
|
197
|
+
const itemsByDecisionRoot = this.itemsByDecisionRootByEpoch.get(epoch);
|
|
198
|
+
const cacheItem = itemsByDecisionRoot?.get(decisionRoot);
|
|
199
|
+
if (itemsByDecisionRoot === undefined || cacheItem === undefined || !isPromiseCacheItem(cacheItem)) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
cacheItem.resolveFn(null);
|
|
204
|
+
itemsByDecisionRoot.delete(decisionRoot);
|
|
205
|
+
if (itemsByDecisionRoot.size === 0) {
|
|
206
|
+
this.itemsByDecisionRootByEpoch.delete(epoch);
|
|
207
|
+
}
|
|
208
|
+
this.metrics?.shufflingCache.cancelledPromises.inc({reason});
|
|
209
|
+
this.logger?.debug("Cancelled shuffling promise", {epoch, decisionRoot, reason});
|
|
210
|
+
}
|
|
211
|
+
|
|
189
212
|
getIndexedAttestation(
|
|
190
213
|
epoch: number,
|
|
191
214
|
decisionRoot: string,
|
|
@@ -205,6 +228,11 @@ export class ShufflingCache {
|
|
|
205
228
|
return this.getBeaconCommittees(epoch, decisionRoot, slot, [index])[0];
|
|
206
229
|
}
|
|
207
230
|
|
|
231
|
+
getBeaconCommitteeOrNull(epoch: number, decisionRoot: string, slot: Slot, index: CommitteeIndex): Uint32Array | null {
|
|
232
|
+
const shuffling = this.getSync(epoch, decisionRoot);
|
|
233
|
+
return shuffling === null ? null : getBeaconCommittees(shuffling, slot, [index])[0];
|
|
234
|
+
}
|
|
235
|
+
|
|
208
236
|
getBeaconCommittees(epoch: number, decisionRoot: string, slot: Slot, indices: CommitteeIndex[]): Uint32Array[] {
|
|
209
237
|
const shuffling = this.getShufflingOrThrow(epoch, decisionRoot);
|
|
210
238
|
return getBeaconCommittees(shuffling, slot, indices);
|
|
@@ -243,8 +271,42 @@ export class ShufflingCache {
|
|
|
243
271
|
}
|
|
244
272
|
// set the shuffling
|
|
245
273
|
shufflingAtEpoch.set(decisionRoot, {type: CacheItemType.shuffling, shuffling});
|
|
246
|
-
|
|
247
|
-
|
|
274
|
+
this.prune();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Keep the `maxEpochs` highest epochs, regardless of insertion order.
|
|
279
|
+
*/
|
|
280
|
+
private prune(): void {
|
|
281
|
+
const toDelete = this.itemsByDecisionRootByEpoch.size - this.maxEpochs;
|
|
282
|
+
if (toDelete <= 0) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const prunedEpochs = Array.from(this.itemsByDecisionRootByEpoch.keys())
|
|
287
|
+
.sort((a, b) => a - b)
|
|
288
|
+
.slice(0, toDelete);
|
|
289
|
+
let prunedShufflings = 0;
|
|
290
|
+
for (const epoch of prunedEpochs) {
|
|
291
|
+
for (const cacheItem of this.itemsByDecisionRootByEpoch.get(epoch)?.values() ?? []) {
|
|
292
|
+
if (isPromiseCacheItem(cacheItem)) {
|
|
293
|
+
cacheItem.resolveFn(null);
|
|
294
|
+
this.metrics?.shufflingCache.cancelledPromises.inc({reason: ShufflingPromiseCancelReason.pruned});
|
|
295
|
+
} else {
|
|
296
|
+
prunedShufflings++;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
this.itemsByDecisionRootByEpoch.delete(epoch);
|
|
300
|
+
}
|
|
301
|
+
this.metrics?.shufflingCache.prunedShufflings.inc(prunedShufflings);
|
|
302
|
+
|
|
303
|
+
this.logger?.verbose("Pruned shuffling cache", {
|
|
304
|
+
prunedEpochs: prunedEpochs.join(","),
|
|
305
|
+
prunedShufflings,
|
|
306
|
+
cachedEpochs: Array.from(this.itemsByDecisionRootByEpoch.keys())
|
|
307
|
+
.sort((a, b) => a - b)
|
|
308
|
+
.join(","),
|
|
309
|
+
});
|
|
248
310
|
}
|
|
249
311
|
}
|
|
250
312
|
|
package/src/db/beacon.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {FlatFileStore} from "./flatFileStore/flatFileStore.js";
|
|
|
8
8
|
import type {FlatFileStoreMetrics} from "./flatFileStore/metrics.js";
|
|
9
9
|
import {IBeaconDb} from "./interface.js";
|
|
10
10
|
import {CheckpointStateRepository} from "./repositories/checkpointState.js";
|
|
11
|
+
import {EarliestAvailableSlotRepository} from "./repositories/earliestAvailableSlot.js";
|
|
11
12
|
import {
|
|
12
13
|
AttesterSlashingRepository,
|
|
13
14
|
BLSToExecutionChangeRepository,
|
|
@@ -48,6 +49,7 @@ export class BeaconDb implements IBeaconDb {
|
|
|
48
49
|
|
|
49
50
|
stateArchive: StateArchiveRepository;
|
|
50
51
|
checkpointState: CheckpointStateRepository;
|
|
52
|
+
earliestAvailableSlot: EarliestAvailableSlotRepository;
|
|
51
53
|
|
|
52
54
|
voluntaryExit: VoluntaryExitRepository;
|
|
53
55
|
proposerSlashing: ProposerSlashingRepository;
|
|
@@ -83,6 +85,7 @@ export class BeaconDb implements IBeaconDb {
|
|
|
83
85
|
|
|
84
86
|
this.stateArchive = new StateArchiveRepository(config, db);
|
|
85
87
|
this.checkpointState = new CheckpointStateRepository(config, db);
|
|
88
|
+
this.earliestAvailableSlot = new EarliestAvailableSlotRepository(db);
|
|
86
89
|
this.voluntaryExit = new VoluntaryExitRepository(config, db);
|
|
87
90
|
this.blsToExecutionChange = new BLSToExecutionChangeRepository(config, db);
|
|
88
91
|
this.proposerSlashing = new ProposerSlashingRepository(config, db);
|
package/src/db/buckets.ts
CHANGED
|
@@ -72,6 +72,7 @@ export enum Bucket {
|
|
|
72
72
|
|
|
73
73
|
gloas_executionPayloadEnvelope = 59, // GLOAS BeaconBlockRoot -> SignedExecutionPayloadEnvelope
|
|
74
74
|
gloas_executionPayloadEnvelopeArchive = 60, // GLOAS Slot -> SignedExecutionPayloadEnvelope, or 0x00 + SignedExecutionPayloadHeaderEnvelope
|
|
75
|
+
earliestAvailableSlot = 61, // singleton -> Slot
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
export function getBucketNameByValue<T extends Bucket>(enumValue: T): keyof typeof Bucket {
|
package/src/db/interface.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {Slot} from "@lodestar/types";
|
|
|
3
3
|
import type {IDataColumnStore} from "./dataColumnStore.js";
|
|
4
4
|
import type {FlatFileStoreMetrics} from "./flatFileStore/metrics.js";
|
|
5
5
|
import {CheckpointStateRepository} from "./repositories/checkpointState.js";
|
|
6
|
+
import {EarliestAvailableSlotRepository} from "./repositories/earliestAvailableSlot.js";
|
|
6
7
|
import {
|
|
7
8
|
AttesterSlashingRepository,
|
|
8
9
|
BLSToExecutionChangeRepository,
|
|
@@ -46,6 +47,7 @@ export interface IBeaconDb {
|
|
|
46
47
|
stateArchive: StateArchiveRepository;
|
|
47
48
|
// checkpoint states
|
|
48
49
|
checkpointState: CheckpointStateRepository;
|
|
50
|
+
earliestAvailableSlot: EarliestAvailableSlotRepository;
|
|
49
51
|
|
|
50
52
|
// op pool
|
|
51
53
|
voluntaryExit: VoluntaryExitRepository;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {Db, encodeKey} from "@lodestar/db";
|
|
2
|
+
import {Slot, ssz} from "@lodestar/types";
|
|
3
|
+
import {Bucket, getBucketNameByValue} from "../buckets.js";
|
|
4
|
+
|
|
5
|
+
export class EarliestAvailableSlotRepository {
|
|
6
|
+
private readonly key = encodeKey(Bucket.earliestAvailableSlot, 0);
|
|
7
|
+
private readonly opts = {bucketId: getBucketNameByValue(Bucket.earliestAvailableSlot)};
|
|
8
|
+
|
|
9
|
+
constructor(private readonly db: Db) {}
|
|
10
|
+
|
|
11
|
+
async get(): Promise<Slot | null> {
|
|
12
|
+
const bytes = await this.db.get(this.key, this.opts);
|
|
13
|
+
return bytes === null ? null : ssz.Slot.deserialize(bytes);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async set(slot: Slot): Promise<void> {
|
|
17
|
+
await this.db.put(this.key, ssz.Slot.serialize(slot), this.opts);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -20,6 +20,7 @@ import {InsertOutcome} from "../../chain/opPools/types.js";
|
|
|
20
20
|
import {RegenCaller, RegenFnName} from "../../chain/regen/interface.js";
|
|
21
21
|
import {ReprocessStatus} from "../../chain/reprocess.js";
|
|
22
22
|
import {RejectReason} from "../../chain/seenCache/seenAttestationData.js";
|
|
23
|
+
import type {ShufflingPromiseCancelReason} from "../../chain/shufflingCache.js";
|
|
23
24
|
import {CacheItemType} from "../../chain/stateCache/types.js";
|
|
24
25
|
import {OpSource} from "../../chain/validatorMonitor.js";
|
|
25
26
|
import type {FlatFileStoreOperation} from "../../db/flatFileStore/metrics.js";
|
|
@@ -1281,6 +1282,10 @@ export function createLodestarMetrics(
|
|
|
1281
1282
|
name: "lodestar_oppool_aggregated_attestation_pool_packed_attestations_seen_committees_total",
|
|
1282
1283
|
help: "Total number of committees for which all members are seen when producing packed attestations",
|
|
1283
1284
|
}),
|
|
1285
|
+
missingShufflingCommittees: register.gauge({
|
|
1286
|
+
name: "lodestar_oppool_aggregated_attestation_pool_packed_attestations_missing_shuffling_committees_total",
|
|
1287
|
+
help: "Total number of committees skipped due to missing shuffling when producing packed attestations",
|
|
1288
|
+
}),
|
|
1284
1289
|
},
|
|
1285
1290
|
},
|
|
1286
1291
|
attestationPool: {
|
|
@@ -1614,6 +1619,19 @@ export function createLodestarMetrics(
|
|
|
1614
1619
|
name: "lodestar_shuffling_cache_size",
|
|
1615
1620
|
help: "Shuffling cache size",
|
|
1616
1621
|
}),
|
|
1622
|
+
epochs: register.gauge({
|
|
1623
|
+
name: "lodestar_shuffling_cache_epochs",
|
|
1624
|
+
help: "Number of epochs in shuffling cache",
|
|
1625
|
+
}),
|
|
1626
|
+
prunedShufflings: register.counter({
|
|
1627
|
+
name: "lodestar_shuffling_cache_pruned_shufflings_total",
|
|
1628
|
+
help: "Total number of shufflings pruned from shuffling cache",
|
|
1629
|
+
}),
|
|
1630
|
+
cancelledPromises: register.counter<{reason: ShufflingPromiseCancelReason}>({
|
|
1631
|
+
name: "lodestar_shuffling_cache_cancelled_promises_total",
|
|
1632
|
+
help: "Total number of shuffling promises resolved with null",
|
|
1633
|
+
labelNames: ["reason"],
|
|
1634
|
+
}),
|
|
1617
1635
|
insertPromiseCount: register.gauge({
|
|
1618
1636
|
name: "lodestar_shuffling_cache_insert_promise_count",
|
|
1619
1637
|
help: "Total number of times insertPromise is called",
|
|
@@ -12,9 +12,10 @@ import {
|
|
|
12
12
|
isForkPostElectra,
|
|
13
13
|
isForkPostGloas,
|
|
14
14
|
} from "@lodestar/params";
|
|
15
|
-
import {computeTimeAtSlot} from "@lodestar/state-transition";
|
|
15
|
+
import {computeTimeAtSlot, signedBlockToSignedHeader} from "@lodestar/state-transition";
|
|
16
16
|
import {
|
|
17
17
|
Root,
|
|
18
|
+
RootHex,
|
|
18
19
|
SignedBeaconBlock,
|
|
19
20
|
SingleAttestation,
|
|
20
21
|
Slot,
|
|
@@ -78,6 +79,7 @@ import {
|
|
|
78
79
|
validateGossipSyncCommittee,
|
|
79
80
|
validateGossipVoluntaryExit,
|
|
80
81
|
validateSyncCommitteeGossipContributionAndProof,
|
|
82
|
+
verifyBlockProposerSignature,
|
|
81
83
|
} from "../../chain/validation/index.js";
|
|
82
84
|
import {validateLightClientFinalityUpdate} from "../../chain/validation/lightClientFinalityUpdate.js";
|
|
83
85
|
import {validateLightClientOptimisticUpdate} from "../../chain/validation/lightClientOptimisticUpdate.js";
|
|
@@ -148,37 +150,16 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH
|
|
|
148
150
|
function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): SequentialGossipHandlers {
|
|
149
151
|
const {chain, config, metrics, logger, core} = modules;
|
|
150
152
|
|
|
151
|
-
|
|
153
|
+
/**
|
|
154
|
+
* Add a gossip block to the seen caches. The proposer signature of the block MUST be verified.
|
|
155
|
+
*/
|
|
156
|
+
function cacheSignatureVerifiedBlock(
|
|
152
157
|
signedBlock: SignedBeaconBlock,
|
|
158
|
+
blockRootHex: RootHex,
|
|
153
159
|
fork: ForkName,
|
|
154
160
|
peerIdStr: string,
|
|
155
161
|
seenTimestampSec: number
|
|
156
|
-
):
|
|
157
|
-
const slot = signedBlock.message.slot;
|
|
158
|
-
const forkTypes = config.getForkTypes(slot);
|
|
159
|
-
const blockRootHex = toRootHex(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message));
|
|
160
|
-
const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec);
|
|
161
|
-
const recvToValLatency = Date.now() / 1000 - seenTimestampSec;
|
|
162
|
-
|
|
163
|
-
// always set block to seen cache for all forks so that we don't need to download it
|
|
164
|
-
// TODO: validate block before adding to cache
|
|
165
|
-
// tracked in https://github.com/ChainSafe/lodestar/issues/7957
|
|
166
|
-
|
|
167
|
-
const logCtx = {
|
|
168
|
-
slot,
|
|
169
|
-
root: blockRootHex,
|
|
170
|
-
currentSlot: chain.clock.currentSlot,
|
|
171
|
-
peerId: peerIdStr,
|
|
172
|
-
delaySec,
|
|
173
|
-
recvToValLatency,
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
logger.debug("Received gossip block", {...logCtx});
|
|
177
|
-
|
|
178
|
-
// optimistically add gossip block to the seen cache
|
|
179
|
-
// if validation fails, we will NOT forward this gossip block to peers
|
|
180
|
-
// - if PARENT_BLOCK_UNKNOWN error, blockInput will then be queued inside BlockInputSync. If the gossip block is really invalid, it will be pruned there
|
|
181
|
-
// - if other validator errors, blockInput will stay in the seen cache and will be pruned on finalization
|
|
162
|
+
): BlockInput {
|
|
182
163
|
const blockInput = chain.seenBlockInputCache.getByBlock({
|
|
183
164
|
block: signedBlock,
|
|
184
165
|
blockRootHex,
|
|
@@ -187,8 +168,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
187
168
|
peerIdStr,
|
|
188
169
|
});
|
|
189
170
|
|
|
190
|
-
//
|
|
191
|
-
// This ensures we have PayloadEnvelopeInput, even through "PARENT_BLOCK_UNKNOWN" error
|
|
171
|
+
// Also seed the payload-envelope cache, so we have PayloadEnvelopeInput even through "PARENT_BLOCK_UNKNOWN" error
|
|
192
172
|
// see https://github.com/ChainSafe/lodestar/issues/9475
|
|
193
173
|
if (isForkPostGloas(fork)) {
|
|
194
174
|
chain.seenPayloadEnvelopeInputCache.add({
|
|
@@ -202,8 +182,37 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
202
182
|
});
|
|
203
183
|
}
|
|
204
184
|
|
|
185
|
+
return blockInput;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function validateBeaconBlock(
|
|
189
|
+
signedBlock: SignedBeaconBlock,
|
|
190
|
+
fork: ForkName,
|
|
191
|
+
peerIdStr: string,
|
|
192
|
+
seenTimestampSec: number
|
|
193
|
+
): Promise<IBlockInput> {
|
|
194
|
+
const slot = signedBlock.message.slot;
|
|
195
|
+
const forkTypes = config.getForkTypes(slot);
|
|
196
|
+
const blockRootHex = toRootHex(forkTypes.BeaconBlock.hashTreeRoot(signedBlock.message));
|
|
197
|
+
const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec);
|
|
198
|
+
const recvToValLatency = Date.now() / 1000 - seenTimestampSec;
|
|
199
|
+
|
|
200
|
+
const logCtx = {
|
|
201
|
+
slot,
|
|
202
|
+
root: blockRootHex,
|
|
203
|
+
currentSlot: chain.clock.currentSlot,
|
|
204
|
+
peerId: peerIdStr,
|
|
205
|
+
delaySec,
|
|
206
|
+
recvToValLatency,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
logger.debug("Received gossip block", {...logCtx});
|
|
210
|
+
|
|
211
|
+
chain.seenBlockInputCache.markValidatingBlock(blockRootHex);
|
|
212
|
+
|
|
205
213
|
try {
|
|
206
214
|
const {skippedSlots} = await validateGossipBlock(config, chain, signedBlock, fork);
|
|
215
|
+
const blockInput = cacheSignatureVerifiedBlock(signedBlock, blockRootHex, fork, peerIdStr, seenTimestampSec);
|
|
207
216
|
|
|
208
217
|
const blockInputMeta = blockInput.getLogMeta();
|
|
209
218
|
|
|
@@ -229,54 +238,54 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
229
238
|
if (e instanceof BlockGossipError) {
|
|
230
239
|
logger.debug("Gossip block has error", {slot, root: blockRootHex, code: e.type.code});
|
|
231
240
|
if (
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
blockInput
|
|
241
|
+
e.type.code === BlockErrorCode.PARENT_BLOCK_UNKNOWN ||
|
|
242
|
+
e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN
|
|
235
243
|
) {
|
|
244
|
+
// The block is retained for unknown parent sync without full validation. Authenticate it first, this
|
|
245
|
+
// does not need the parent state
|
|
246
|
+
const proposerIndex = signedBlock.message.proposerIndex;
|
|
247
|
+
await verifyBlockProposerSignature(chain, signedBlock, blockRootHex);
|
|
248
|
+
chain.seenBlockProposers.observeBlockRoot(
|
|
249
|
+
slot,
|
|
250
|
+
proposerIndex,
|
|
251
|
+
blockRootHex,
|
|
252
|
+
signedBlockToSignedHeader(config, signedBlock)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
// Check again after the signature verification so concurrent proposals cannot both be retained
|
|
256
|
+
if (chain.seenBlockProposers.isKnown(slot, proposerIndex)) {
|
|
257
|
+
if (chain.seenBlockProposers.isRepeatProposal(slot, proposerIndex, blockRootHex)) {
|
|
258
|
+
throw new BlockGossipError(GossipAction.IGNORE, {
|
|
259
|
+
code: BlockErrorCode.REPEAT_PROPOSAL,
|
|
260
|
+
proposerIndex,
|
|
261
|
+
root: blockRootHex,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRootHex});
|
|
265
|
+
}
|
|
266
|
+
chain.seenBlockProposers.add(slot, proposerIndex, blockRootHex);
|
|
267
|
+
|
|
268
|
+
const blockInput = cacheSignatureVerifiedBlock(signedBlock, blockRootHex, fork, peerIdStr, seenTimestampSec);
|
|
236
269
|
chain.emitter.emit(ChainEvent.blockUnknownParent, {
|
|
237
270
|
blockInput,
|
|
238
271
|
peer: peerIdStr,
|
|
239
272
|
source: BlockInputSource.gossip,
|
|
240
273
|
});
|
|
241
|
-
// throw error (don't prune the blockInput)
|
|
242
274
|
throw e;
|
|
243
275
|
}
|
|
244
276
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
e.type.code === BlockErrorCode.REPEAT_PROPOSAL &&
|
|
254
|
-
!chain.seenBlockProposers.hasBlockRoot(slot, signedBlock.message.proposerIndex, blockRootHex)
|
|
255
|
-
) {
|
|
256
|
-
chain.seenBlockInputCache.remove(blockRootHex);
|
|
257
|
-
if (isForkPostGloas(fork)) {
|
|
258
|
-
chain.seenPayloadEnvelopeInputCache.remove(blockRootHex);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
throw e;
|
|
277
|
+
if (e.action === GossipAction.REJECT) {
|
|
278
|
+
chain.persistInvalidSszValue(
|
|
279
|
+
forkTypes.SignedBeaconBlock,
|
|
280
|
+
signedBlock,
|
|
281
|
+
`gossip_reject_slot_${slot}`,
|
|
282
|
+
blockRootHex
|
|
283
|
+
);
|
|
262
284
|
}
|
|
263
|
-
|
|
264
|
-
chain.persistInvalidSszValue(
|
|
265
|
-
forkTypes.SignedBeaconBlock,
|
|
266
|
-
signedBlock,
|
|
267
|
-
`gossip_reject_slot_${slot}`,
|
|
268
|
-
blockRootHex
|
|
269
|
-
);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// REJECT or unexpected (non-BlockGossipError) error: drop the optimistically-added entries from
|
|
273
|
-
// both caches, keeping them consistent. The block may carry any parent root, so only its own entry is removed
|
|
274
|
-
chain.seenBlockInputCache.remove(blockRootHex);
|
|
275
|
-
if (isForkPostGloas(fork)) {
|
|
276
|
-
chain.seenPayloadEnvelopeInputCache.remove(blockRootHex);
|
|
277
285
|
}
|
|
278
286
|
throw e;
|
|
279
287
|
} finally {
|
|
288
|
+
chain.seenBlockInputCache.unmarkValidatingBlock(blockRootHex);
|
|
280
289
|
// The block received from the network may have established an equivocation, either by conflicting
|
|
281
290
|
// with a previously observed block root (REPEAT_PROPOSAL) or with a root observed during validation
|
|
282
291
|
const proposerIndex = signedBlock.message.proposerIndex;
|
|
@@ -752,18 +761,23 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
752
761
|
e.type.code === BlockErrorCode.REPEAT_PROPOSAL &&
|
|
753
762
|
// Only a signature-verified sibling recorded in the seen cache is imported. The cache holds at most two
|
|
754
763
|
// roots per proposer and slot, which bounds full imports over gossip to one alternate
|
|
755
|
-
chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root)
|
|
764
|
+
chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root) &&
|
|
765
|
+
// A sibling with an unknown parent cannot be imported, sync fetches it if its branch becomes relevant
|
|
766
|
+
chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) !== null
|
|
756
767
|
) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
768
|
+
const blockInput = cacheSignatureVerifiedBlock(
|
|
769
|
+
signedBlock,
|
|
770
|
+
e.type.root,
|
|
771
|
+
topic.boundary.fork,
|
|
772
|
+
peerIdStr,
|
|
773
|
+
seenTimestampSec
|
|
774
|
+
);
|
|
775
|
+
// We're returning IGNORE (thrown below), so this block is not forwarded to peers. Unlike the
|
|
776
|
+
// happy path there is no rush to forward, so we don't need to wrap in callInNextEventLoop.
|
|
777
|
+
chain.serializedCache.set(signedBlock, serializedData);
|
|
778
|
+
// this is technically not a valid gossip block but gossip validation is a cheap subset of checks
|
|
779
|
+
// this runs the full state transition, so importing an equivocating-but-valid block here is safe.
|
|
780
|
+
handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
|
|
767
781
|
}
|
|
768
782
|
// rethrow so gossipValidatorFn maps IGNORE -> TopicValidatorResult.Ignore (message not forwarded)
|
|
769
783
|
throw e;
|
package/src/node/nodejs.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {BeaconConfig} from "@lodestar/config";
|
|
|
14
14
|
import type {LoggerNode} from "@lodestar/logger/node";
|
|
15
15
|
import {ZERO_HASH_HEX} from "@lodestar/params";
|
|
16
16
|
import {IBeaconStateView, isStatePostBellatrix, isStatePostGloas} from "@lodestar/state-transition";
|
|
17
|
+
import {Slot} from "@lodestar/types";
|
|
17
18
|
import {sleep, toRootHex} from "@lodestar/utils";
|
|
18
19
|
import {ProcessShutdownCallback} from "@lodestar/validator";
|
|
19
20
|
import {BeaconRestApiServer, getApi} from "../api/index.js";
|
|
@@ -61,6 +62,7 @@ export type BeaconNodeInitModules = {
|
|
|
61
62
|
peerStoreDir?: string;
|
|
62
63
|
anchorState: IBeaconStateView;
|
|
63
64
|
isAnchorStateFinalized: boolean;
|
|
65
|
+
earliestAvailableSlot: Slot;
|
|
64
66
|
metricsRegistries?: Registry[];
|
|
65
67
|
};
|
|
66
68
|
|
|
@@ -159,6 +161,7 @@ export class BeaconNode {
|
|
|
159
161
|
peerStoreDir,
|
|
160
162
|
anchorState,
|
|
161
163
|
isAnchorStateFinalized,
|
|
164
|
+
earliestAvailableSlot,
|
|
162
165
|
metricsRegistries = [],
|
|
163
166
|
}: BeaconNodeInitModules): Promise<T> {
|
|
164
167
|
if (hasher.name !== "hashtree") {
|
|
@@ -266,6 +269,7 @@ export class BeaconNode {
|
|
|
266
269
|
validatorMonitor,
|
|
267
270
|
anchorState,
|
|
268
271
|
isAnchorStateFinalized,
|
|
272
|
+
earliestAvailableSlot,
|
|
269
273
|
executionEngine: initializeExecutionEngine(executionEngineOpts, {
|
|
270
274
|
metrics,
|
|
271
275
|
signal,
|
package/src/sync/range/batch.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {IClock} from "../../util/clock.js";
|
|
|
14
14
|
import {CustodyConfig} from "../../util/dataColumns.js";
|
|
15
15
|
import {PeerIdStr} from "../../util/peerId.js";
|
|
16
16
|
import {MAX_BATCH_DOWNLOAD_ATTEMPTS, MAX_BATCH_PROCESSING_ATTEMPTS} from "../constants.js";
|
|
17
|
-
import {DownloadByRangeRequests,
|
|
17
|
+
import {DownloadByRangeRequests, ParentPayload} from "../utils/downloadByRange.js";
|
|
18
18
|
import {getBatchSlotRange, hashBlocks} from "./utils/index.js";
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -175,7 +175,7 @@ export class Batch {
|
|
|
175
175
|
private readonly clock: IClock;
|
|
176
176
|
private readonly custodyConfig: CustodyConfig;
|
|
177
177
|
private readonly isFirstBatchInChain: boolean;
|
|
178
|
-
private readonly
|
|
178
|
+
private readonly parentPayload: ParentPayload | undefined;
|
|
179
179
|
|
|
180
180
|
constructor(
|
|
181
181
|
startEpoch: Epoch,
|
|
@@ -183,7 +183,7 @@ export class Batch {
|
|
|
183
183
|
clock: IClock,
|
|
184
184
|
custodyConfig: CustodyConfig,
|
|
185
185
|
isFirstBatchInChain: boolean,
|
|
186
|
-
|
|
186
|
+
parentPayload: ParentPayload | undefined,
|
|
187
187
|
targetSlot: Slot
|
|
188
188
|
) {
|
|
189
189
|
this.config = config;
|
|
@@ -196,7 +196,7 @@ export class Batch {
|
|
|
196
196
|
this.startSlot = startSlot;
|
|
197
197
|
this.count = Math.min(count, targetSlot - startSlot + 1);
|
|
198
198
|
this.isFirstBatchInChain = isFirstBatchInChain;
|
|
199
|
-
this.
|
|
199
|
+
this.parentPayload = parentPayload;
|
|
200
200
|
this.requests = this.getRequests([]);
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -208,24 +208,19 @@ export class Batch {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// we only know if we should download parent envelope if firstBlock is downloaded
|
|
211
|
-
if (firstBlock === undefined) return false;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
.
|
|
215
|
-
|
|
211
|
+
if (firstBlock === undefined || this.parentPayload === undefined) return false;
|
|
212
|
+
const firstBlockBid = (firstBlock.message.body as gloas.BeaconBlockBody).signedExecutionPayloadBid.message;
|
|
213
|
+
return (
|
|
214
|
+
byteArrayEquals(firstBlock.message.parentRoot, this.parentPayload.blockRoot) &&
|
|
215
|
+
byteArrayEquals(firstBlockBid.parentBlockHash, this.parentPayload.bid.blockHash)
|
|
216
|
+
);
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
|
|
219
|
-
if (this.
|
|
220
|
-
throw new Error(
|
|
221
|
-
`Coding error: getParentPayloadCommitments called without latestBid for parentBlockRoot=${toRootHex(parentBlockRoot)}`
|
|
222
|
-
);
|
|
219
|
+
getParentPayload(): ParentPayload {
|
|
220
|
+
if (this.parentPayload === undefined) {
|
|
221
|
+
throw new Error("Coding error: getParentPayload called without parentPayload");
|
|
223
222
|
}
|
|
224
|
-
return
|
|
225
|
-
blockRoot: parentBlockRoot,
|
|
226
|
-
blockRootHex: toRootHex(parentBlockRoot),
|
|
227
|
-
kzgCommitments: this.latestBid.blobKzgCommitments,
|
|
228
|
-
};
|
|
223
|
+
return this.parentPayload;
|
|
229
224
|
}
|
|
230
225
|
|
|
231
226
|
/**
|