@lodestar/beacon-node 1.50.0-dev.ba0411b482 → 1.50.0-dev.c02b971f5b
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/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 +7 -3
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/chain.d.ts +2 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +6 -4
- package/lib/chain/chain.js.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/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 +1 -0
- package/lib/chain/shufflingCache.d.ts.map +1 -1
- package/lib/chain/shufflingCache.js +4 -0
- 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 +1 -0
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +4 -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/chain/archiveStore/archiveStore.ts +18 -24
- package/src/chain/archiveStore/utils/pruneHistory.ts +4 -1
- package/src/chain/blocks/importBlock.ts +9 -3
- package/src/chain/chain.ts +9 -3
- package/src/chain/opPools/aggregatedAttestationPool.ts +34 -10
- package/src/chain/seenCache/seenGossipBlockInput.ts +23 -0
- package/src/chain/shufflingCache.ts +5 -0
- 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 +4 -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
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
isStatePostGloas,
|
|
31
31
|
} from "@lodestar/state-transition";
|
|
32
32
|
import {Attestation, Epoch, RootHex, Slot, electra, isElectraAttestation, phase0, ssz} from "@lodestar/types";
|
|
33
|
-
import {MapDef, assert, toRootHex} from "@lodestar/utils";
|
|
33
|
+
import {Logger, MapDef, assert, toRootHex} from "@lodestar/utils";
|
|
34
34
|
import {Metrics} from "../../metrics/metrics.js";
|
|
35
35
|
import {IntersectResult, intersectUint8Arrays} from "../../util/bitArray.js";
|
|
36
36
|
import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
|
|
@@ -58,7 +58,7 @@ export type AttestationsConsolidation = {
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* This function returns not seen participation for a given epoch and slot and committee index.
|
|
61
|
-
* Return
|
|
61
|
+
* Return an empty set if all validators are seen, or null if the committee cannot be computed (e.g. missing shuffling).
|
|
62
62
|
*/
|
|
63
63
|
type GetNotSeenValidatorsFn = (epoch: Epoch, slot: Slot, committeeIndex: number) => Set<number> | null;
|
|
64
64
|
|
|
@@ -140,7 +140,8 @@ export class AggregatedAttestationPool {
|
|
|
140
140
|
|
|
141
141
|
constructor(
|
|
142
142
|
private readonly config: BeaconConfig,
|
|
143
|
-
private readonly metrics: Metrics | null = null
|
|
143
|
+
private readonly metrics: Metrics | null = null,
|
|
144
|
+
private readonly logger: Logger | null = null
|
|
144
145
|
) {
|
|
145
146
|
metrics?.opPool.aggregatedAttestationPool.attDataPerSlot.addCollect(() => this.onScrapeMetrics(metrics));
|
|
146
147
|
}
|
|
@@ -244,6 +245,7 @@ export class AggregatedAttestationPool {
|
|
|
244
245
|
const consolidations = new Map<AttestationsConsolidation, number>();
|
|
245
246
|
let scannedSlots = 0;
|
|
246
247
|
let stopReason: ScannedSlotsTerminationReason | null = null;
|
|
248
|
+
const missingShufflingCommitteesByEpoch = new Map<Epoch, number>();
|
|
247
249
|
slot: for (const slot of slots) {
|
|
248
250
|
const attestationGroupByIndexByDataHash = this.attestationGroupByIndexByDataHexBySlot.get(slot);
|
|
249
251
|
// should not happen
|
|
@@ -295,7 +297,12 @@ export class AggregatedAttestationPool {
|
|
|
295
297
|
|
|
296
298
|
for (const [committeeIndex, attestationGroup] of attestationGroupByIndex.entries()) {
|
|
297
299
|
const notSeenCommitteeMembers = notSeenValidatorsFn(epoch, slot, committeeIndex);
|
|
298
|
-
if (notSeenCommitteeMembers === null
|
|
300
|
+
if (notSeenCommitteeMembers === null) {
|
|
301
|
+
// skip this committee instead of failing the whole block production, other epochs may still have shuffling
|
|
302
|
+
missingShufflingCommitteesByEpoch.set(epoch, (missingShufflingCommitteesByEpoch.get(epoch) ?? 0) + 1);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (notSeenCommitteeMembers.size === 0) {
|
|
299
306
|
this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.seenCommittees.inc();
|
|
300
307
|
continue;
|
|
301
308
|
}
|
|
@@ -386,6 +393,18 @@ export class AggregatedAttestationPool {
|
|
|
386
393
|
scannedSlots++;
|
|
387
394
|
}
|
|
388
395
|
|
|
396
|
+
for (const [epoch, missingCommittees] of missingShufflingCommitteesByEpoch) {
|
|
397
|
+
this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.missingShufflingCommittees.inc(
|
|
398
|
+
missingCommittees
|
|
399
|
+
);
|
|
400
|
+
this.logger?.debug("Missing shuffling when packing attestations, skipped committees", {
|
|
401
|
+
slot: stateSlot,
|
|
402
|
+
epoch,
|
|
403
|
+
decisionRoot: state.getShufflingDecisionRoot(epoch),
|
|
404
|
+
missingCommittees,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
389
408
|
this.metrics?.opPool.aggregatedAttestationPool.packedAttestations.totalConsolidations.set(consolidations.size);
|
|
390
409
|
|
|
391
410
|
const sortedConsolidationsByScore = Array.from(consolidations.entries())
|
|
@@ -770,13 +789,18 @@ export function getNotSeenValidatorsFn(
|
|
|
770
789
|
}
|
|
771
790
|
const cacheKey = slot + "_" + committeeIndex;
|
|
772
791
|
let notSeenCommitteeMembers = cachedNotSeenValidators.get(cacheKey);
|
|
773
|
-
if (notSeenCommitteeMembers
|
|
774
|
-
|
|
775
|
-
return notSeenCommitteeMembers.size === 0 ? null : notSeenCommitteeMembers;
|
|
792
|
+
if (notSeenCommitteeMembers !== undefined) {
|
|
793
|
+
return notSeenCommitteeMembers;
|
|
776
794
|
}
|
|
777
795
|
|
|
778
796
|
const decisionRoot = state.getShufflingDecisionRoot(computeEpochAtSlot(slot));
|
|
779
|
-
const committee = shufflingCache.
|
|
797
|
+
const committee = shufflingCache.getBeaconCommitteeOrNull(epoch, decisionRoot, slot, committeeIndex);
|
|
798
|
+
if (committee === null) {
|
|
799
|
+
// attestations in the pool were validated against a cached shuffling of the previous or current epoch,
|
|
800
|
+
// so a miss is a ShufflingCache bug. Return null so the caller tracks it via metrics/logs without failing
|
|
801
|
+
// block production. Don't fall back to the state's shuffling, that would hide the bug
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
780
804
|
notSeenCommitteeMembers = new Set<number>();
|
|
781
805
|
for (const [i, validatorIndex] of committee.entries()) {
|
|
782
806
|
// no need to check flagIsTimelySource as if validator is not seen, it's participation status is 0
|
|
@@ -786,8 +810,8 @@ export function getNotSeenValidatorsFn(
|
|
|
786
810
|
}
|
|
787
811
|
}
|
|
788
812
|
cachedNotSeenValidators.set(cacheKey, notSeenCommitteeMembers);
|
|
789
|
-
//
|
|
790
|
-
return notSeenCommitteeMembers
|
|
813
|
+
// an empty set means all validators are seen, we don't need to check for any attestations of same committee again
|
|
814
|
+
return notSeenCommitteeMembers;
|
|
791
815
|
};
|
|
792
816
|
}
|
|
793
817
|
|
|
@@ -111,6 +111,9 @@ export class SeenBlockInput {
|
|
|
111
111
|
// there should only 1 block root per slot but we need to always compare against rootHex
|
|
112
112
|
// and the signature to ensure we only skip verification if both match
|
|
113
113
|
private verifiedProposerSignatures = new Map<Slot, Map<RootHex, BLSSignature>>();
|
|
114
|
+
// Roots of gossip blocks under validation, counted because blocks with the same root but different signatures
|
|
115
|
+
// can be validated concurrently
|
|
116
|
+
private validatingBlockRoots = new Map<RootHex, number>();
|
|
114
117
|
|
|
115
118
|
constructor({
|
|
116
119
|
config,
|
|
@@ -153,6 +156,26 @@ export class SeenBlockInput {
|
|
|
153
156
|
return this.blockInputs.get(rootHex)?.hasBlock() ?? false;
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Mark a gossip block root as under validation, every call MUST be paired with `unmarkValidatingBlock()`
|
|
161
|
+
*/
|
|
162
|
+
markValidatingBlock(rootHex: RootHex): void {
|
|
163
|
+
this.validatingBlockRoots.set(rootHex, (this.validatingBlockRoots.get(rootHex) ?? 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
unmarkValidatingBlock(rootHex: RootHex): void {
|
|
167
|
+
const count = this.validatingBlockRoots.get(rootHex) ?? 0;
|
|
168
|
+
if (count <= 1) {
|
|
169
|
+
this.validatingBlockRoots.delete(rootHex);
|
|
170
|
+
} else {
|
|
171
|
+
this.validatingBlockRoots.set(rootHex, count - 1);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
isValidatingBlock(rootHex: RootHex): boolean {
|
|
176
|
+
return this.validatingBlockRoots.has(rootHex);
|
|
177
|
+
}
|
|
178
|
+
|
|
156
179
|
get(rootHex: RootHex): IBlockInput | undefined {
|
|
157
180
|
return this.blockInputs.get(rootHex);
|
|
158
181
|
}
|
|
@@ -205,6 +205,11 @@ export class ShufflingCache {
|
|
|
205
205
|
return this.getBeaconCommittees(epoch, decisionRoot, slot, [index])[0];
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
getBeaconCommitteeOrNull(epoch: number, decisionRoot: string, slot: Slot, index: CommitteeIndex): Uint32Array | null {
|
|
209
|
+
const shuffling = this.getSync(epoch, decisionRoot);
|
|
210
|
+
return shuffling === null ? null : getBeaconCommittees(shuffling, slot, [index])[0];
|
|
211
|
+
}
|
|
212
|
+
|
|
208
213
|
getBeaconCommittees(epoch: number, decisionRoot: string, slot: Slot, indices: CommitteeIndex[]): Uint32Array[] {
|
|
209
214
|
const shuffling = this.getShufflingOrThrow(epoch, decisionRoot);
|
|
210
215
|
return getBeaconCommittees(shuffling, slot, indices);
|
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
|
+
}
|
|
@@ -1281,6 +1281,10 @@ export function createLodestarMetrics(
|
|
|
1281
1281
|
name: "lodestar_oppool_aggregated_attestation_pool_packed_attestations_seen_committees_total",
|
|
1282
1282
|
help: "Total number of committees for which all members are seen when producing packed attestations",
|
|
1283
1283
|
}),
|
|
1284
|
+
missingShufflingCommittees: register.gauge({
|
|
1285
|
+
name: "lodestar_oppool_aggregated_attestation_pool_packed_attestations_missing_shuffling_committees_total",
|
|
1286
|
+
help: "Total number of committees skipped due to missing shuffling when producing packed attestations",
|
|
1287
|
+
}),
|
|
1284
1288
|
},
|
|
1285
1289
|
},
|
|
1286
1290
|
attestationPool: {
|
|
@@ -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
|
/**
|
package/src/sync/range/chain.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {ChainForkConfig} from "@lodestar/config";
|
|
2
|
-
import {Epoch, Root, Slot
|
|
2
|
+
import {Epoch, Root, Slot} from "@lodestar/types";
|
|
3
3
|
import {ErrorAborted, LodestarError, Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
|
|
4
4
|
import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
|
|
5
5
|
import {BlockInputErrorCode} from "../../chain/blocks/blockInput/errors.js";
|
|
@@ -16,7 +16,7 @@ import {ItTrigger} from "../../util/itTrigger.js";
|
|
|
16
16
|
import {PeerIdStr} from "../../util/peerId.js";
|
|
17
17
|
import {WarnResult, wrapError} from "../../util/wrapError.js";
|
|
18
18
|
import {BATCH_BUFFER_SIZE, EPOCHS_PER_BATCH, MAX_LOOK_AHEAD_EPOCHS} from "../constants.js";
|
|
19
|
-
import {DownloadByRangeError, DownloadByRangeErrorCode} from "../utils/downloadByRange.js";
|
|
19
|
+
import {DownloadByRangeError, DownloadByRangeErrorCode, ParentPayload} from "../utils/downloadByRange.js";
|
|
20
20
|
import {getRateLimitedUntilMs} from "../utils/rateLimit.js";
|
|
21
21
|
import {RangeSyncType} from "../utils/remoteSyncType.js";
|
|
22
22
|
import {Batch, BatchError, BatchErrorCode, BatchMetadata, BatchStatus} from "./batch.js";
|
|
@@ -160,7 +160,7 @@ export class SyncChain {
|
|
|
160
160
|
private readonly clock: IClock;
|
|
161
161
|
private readonly metrics: Metrics | null;
|
|
162
162
|
private readonly custodyConfig: CustodyConfig;
|
|
163
|
-
private readonly
|
|
163
|
+
private readonly parentPayload: ParentPayload | undefined;
|
|
164
164
|
|
|
165
165
|
constructor(
|
|
166
166
|
initialBatchEpoch: Epoch,
|
|
@@ -168,7 +168,7 @@ export class SyncChain {
|
|
|
168
168
|
syncType: RangeSyncType,
|
|
169
169
|
fns: SyncChainFns,
|
|
170
170
|
modules: SyncChainModules,
|
|
171
|
-
|
|
171
|
+
parentPayload: ParentPayload | undefined
|
|
172
172
|
) {
|
|
173
173
|
const {config, clock, custodyConfig, logger, metrics} = modules;
|
|
174
174
|
this.firstBatchEpoch = initialBatchEpoch;
|
|
@@ -184,7 +184,7 @@ export class SyncChain {
|
|
|
184
184
|
this.clock = clock;
|
|
185
185
|
this.metrics = metrics;
|
|
186
186
|
this.custodyConfig = custodyConfig;
|
|
187
|
-
this.
|
|
187
|
+
this.parentPayload = parentPayload;
|
|
188
188
|
this.logger = logger;
|
|
189
189
|
this.logId = `${syncType}-${nextChainId++}`;
|
|
190
190
|
|
|
@@ -536,8 +536,8 @@ export class SyncChain {
|
|
|
536
536
|
this.clock,
|
|
537
537
|
this.custodyConfig,
|
|
538
538
|
this.isFirstBatch,
|
|
539
|
-
// `
|
|
540
|
-
this.isFirstBatch ? this.
|
|
539
|
+
// `parentPayload` is only meaningful for the first batch
|
|
540
|
+
this.isFirstBatch ? this.parentPayload : undefined,
|
|
541
541
|
this.target.slot
|
|
542
542
|
);
|
|
543
543
|
this.isFirstBatch = false;
|
|
@@ -586,6 +586,8 @@ export class SyncChain {
|
|
|
586
586
|
case DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS:
|
|
587
587
|
case DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH:
|
|
588
588
|
case DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT:
|
|
589
|
+
case DownloadByRangeErrorCode.INVALID_ENVELOPE_BLOCK_HASH:
|
|
590
|
+
case DownloadByRangeErrorCode.INVALID_ENVELOPE_SLOT:
|
|
589
591
|
case DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT:
|
|
590
592
|
case BlobSidecarErrorCode.INCLUSION_PROOF_INVALID:
|
|
591
593
|
case BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH:
|