@lodestar/beacon-node 1.44.0 → 1.45.0-dev.29704bdb26
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/blocks/importBlock.d.ts.map +1 -1
- package/lib/chain/blocks/importBlock.js +8 -3
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts +5 -2
- package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeProcessor.js +17 -17
- package/lib/chain/blocks/payloadEnvelopeProcessor.js.map +1 -1
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +1 -0
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +7 -0
- package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
- package/lib/chain/shufflingCache.d.ts +6 -0
- package/lib/chain/shufflingCache.d.ts.map +1 -1
- package/lib/chain/shufflingCache.js +21 -6
- package/lib/chain/shufflingCache.js.map +1 -1
- package/lib/network/peers/discover.d.ts.map +1 -1
- package/lib/network/peers/discover.js +17 -12
- package/lib/network/peers/discover.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +26 -13
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
- package/lib/sync/utils/downloadByRange.js +25 -19
- package/lib/sync/utils/downloadByRange.js.map +1 -1
- package/package.json +14 -14
- package/src/chain/blocks/importBlock.ts +17 -3
- package/src/chain/blocks/payloadEnvelopeProcessor.ts +17 -17
- package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +8 -0
- package/src/chain/shufflingCache.ts +23 -7
- package/src/network/peers/discover.ts +16 -11
- package/src/network/processor/gossipHandlers.ts +29 -14
- package/src/sync/utils/downloadByRange.ts +26 -19
|
@@ -8,28 +8,24 @@ import {ImportPayloadOpts} from "./types.js";
|
|
|
8
8
|
// TODO GLOAS: Set to be equal to DEFAULT_MAX_PENDING_UNFINALIZED_PAYLOAD_ENVELOPE_WRITES for now
|
|
9
9
|
const QUEUE_MAX_LENGTH = 16;
|
|
10
10
|
|
|
11
|
-
enum PayloadEnvelopeImportStatus {
|
|
12
|
-
queued = "queued",
|
|
13
|
-
importing = "importing",
|
|
14
|
-
imported = "imported",
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
/**
|
|
18
12
|
* PayloadEnvelopeProcessor processes payload envelope jobs in a queued fashion, one after the other.
|
|
19
13
|
*
|
|
20
14
|
* Jobs are enqueued only on envelope arrival (gossip or API). The envelope may reach us before
|
|
21
15
|
* the sampled data columns; importExecutionPayload awaits `verifyPayloadsDataAvailability`
|
|
22
16
|
* internally, so a queued job can pend for up to `PAYLOAD_DATA_AVAILABILITY_TIMEOUT` while
|
|
23
|
-
* waiting for columns. Duplicate triggers for the same payloadInput are deduped
|
|
17
|
+
* waiting for columns. Duplicate triggers for the same payloadInput are deduped by sharing the
|
|
18
|
+
* in-flight import promise: every caller observes the real import outcome. Resolving duplicates
|
|
19
|
+
* early instead would let BlockInputSync mark a still-pending payload as processed, re-queue it,
|
|
20
|
+
* and spin re-processing the same root until OOM while the import is blocked (e.g. EL syncing).
|
|
24
21
|
*/
|
|
25
22
|
export class PayloadEnvelopeProcessor {
|
|
26
23
|
readonly jobQueue: JobItemQueue<[PayloadEnvelopeInput, ImportPayloadOpts], void>;
|
|
27
|
-
private readonly
|
|
24
|
+
private readonly imports = new WeakMap<PayloadEnvelopeInput, Promise<void>>();
|
|
28
25
|
|
|
29
26
|
constructor(chain: BeaconChain, metrics: Metrics | null, signal: AbortSignal) {
|
|
30
27
|
this.jobQueue = new JobItemQueue<[PayloadEnvelopeInput, ImportPayloadOpts], void>(
|
|
31
28
|
(payloadInput, opts) => {
|
|
32
|
-
this.importStatus.set(payloadInput, PayloadEnvelopeImportStatus.importing);
|
|
33
29
|
return processExecutionPayload.call(chain, payloadInput, signal, opts);
|
|
34
30
|
},
|
|
35
31
|
{maxLength: QUEUE_MAX_LENGTH, noYieldIfOneItem: true, signal},
|
|
@@ -38,24 +34,28 @@ export class PayloadEnvelopeProcessor {
|
|
|
38
34
|
}
|
|
39
35
|
|
|
40
36
|
async processPayloadEnvelopeJob(payloadInput: PayloadEnvelopeInput, opts: ImportPayloadOpts = {}): Promise<void> {
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
const existing = this.imports.get(payloadInput);
|
|
38
|
+
if (existing) {
|
|
39
|
+
return existing;
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
await this.jobQueue.waitForSpace();
|
|
46
43
|
|
|
47
44
|
// Re-check after await, as another call may have queued this payload.
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const queued = this.imports.get(payloadInput);
|
|
46
|
+
if (queued) {
|
|
47
|
+
return queued;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
this.
|
|
50
|
+
const importPromise = this.jobQueue.push(payloadInput, opts);
|
|
51
|
+
this.imports.set(payloadInput, importPromise);
|
|
53
52
|
|
|
54
53
|
try {
|
|
55
|
-
await
|
|
56
|
-
this.importStatus.set(payloadInput, PayloadEnvelopeImportStatus.imported);
|
|
54
|
+
await importPromise;
|
|
57
55
|
} catch (e) {
|
|
58
|
-
|
|
56
|
+
// Drop the failed import so a later attempt can retry once the failure cause is resolved
|
|
57
|
+
// (e.g. BLOCK_NOT_IN_FORK_CHOICE after the block lands in fork choice)
|
|
58
|
+
this.imports.delete(payloadInput);
|
|
59
59
|
throw e;
|
|
60
60
|
}
|
|
61
61
|
}
|
|
@@ -152,6 +152,14 @@ export class SeenPayloadEnvelopeInput {
|
|
|
152
152
|
return this.payloadInputs.size;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
prune(blockRootHex: RootHex): void {
|
|
156
|
+
const input = this.payloadInputs.get(blockRootHex);
|
|
157
|
+
if (input) {
|
|
158
|
+
this.evictPayloadInput(input);
|
|
159
|
+
this.logger?.verbose("SeenPayloadEnvelopeInput.prune deleted", {slot: input.slot, root: blockRootHex});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
155
163
|
pruneBelowParent(parentBlock: ProtoBlock): void {
|
|
156
164
|
for (const block of this.forkChoice.getAllAncestorBlocks(parentBlock.blockRoot, parentBlock.payloadStatus)) {
|
|
157
165
|
// Only evict once the payload is FULL (revealed/imported) — on an EMPTY/PENDING branch we may
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {ForkSeq} from "@lodestar/params";
|
|
1
|
+
import {ForkSeq, GENESIS_EPOCH} from "@lodestar/params";
|
|
2
2
|
import {
|
|
3
3
|
EpochShuffling,
|
|
4
4
|
IBeaconStateView,
|
|
@@ -155,19 +155,35 @@ export class ShufflingCache {
|
|
|
155
155
|
return null;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Check if a shuffling is cached by (epoch, decisionRoot).
|
|
160
|
+
* For native bindings, this avoids having to memoize heavy epoch shufflings within
|
|
161
|
+
* `NativeBeaconStateView`.
|
|
162
|
+
*/
|
|
163
|
+
has(epoch: Epoch, decisionRoot: RootHex): boolean {
|
|
164
|
+
const cacheItem = this.itemsByDecisionRootByEpoch.getOrDefault(epoch).get(decisionRoot);
|
|
165
|
+
return cacheItem !== undefined && isShufflingCacheItem(cacheItem);
|
|
166
|
+
}
|
|
167
|
+
|
|
158
168
|
/**
|
|
159
169
|
* Process a state to extract and cache all shufflings (previous, current, next).
|
|
160
170
|
* Uses the stored decision roots from epochCtx.
|
|
161
171
|
*/
|
|
162
172
|
processState(state: IBeaconStateView): void {
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
const currentEpoch = state.epoch;
|
|
174
|
+
const previousEpoch = currentEpoch === GENESIS_EPOCH ? GENESIS_EPOCH : currentEpoch - 1;
|
|
165
175
|
|
|
166
|
-
|
|
167
|
-
|
|
176
|
+
if (!this.has(previousEpoch, state.previousDecisionRoot)) {
|
|
177
|
+
this.set(state.getPreviousShuffling(), state.previousDecisionRoot);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!this.has(currentEpoch, state.currentDecisionRoot)) {
|
|
181
|
+
this.set(state.getCurrentShuffling(), state.currentDecisionRoot);
|
|
182
|
+
}
|
|
168
183
|
|
|
169
|
-
|
|
170
|
-
|
|
184
|
+
if (!this.has(currentEpoch + 1, state.nextDecisionRoot)) {
|
|
185
|
+
this.set(state.getNextShuffling(), state.nextDecisionRoot);
|
|
186
|
+
}
|
|
171
187
|
}
|
|
172
188
|
|
|
173
189
|
getIndexedAttestation(
|
|
@@ -141,6 +141,22 @@ export class PeerDiscovery {
|
|
|
141
141
|
this.discv5FirstQueryDelayMs = opts.discv5FirstQueryDelayMs;
|
|
142
142
|
this.connectToDiscv5BootnodesOnStart = opts.connectToDiscv5Bootnodes;
|
|
143
143
|
|
|
144
|
+
// Transport tags vary by library: @libp2p/tcp uses '@libp2p/tcp', @chainsafe/libp2p-quic uses 'quic'
|
|
145
|
+
// Normalize to simple 'tcp' / 'quic' strings for matching.
|
|
146
|
+
// Must be initialized before the discovery listeners are registered and the bootENRs are processed
|
|
147
|
+
// below, since handleDiscoveredPeer() reads this.transports. Previously this was assigned at the end
|
|
148
|
+
// of the constructor, so when network.connectToDiscv5Bootnodes is enabled every bootENR was processed
|
|
149
|
+
// while this.transports was still undefined, throwing "Cannot read properties of undefined (reading
|
|
150
|
+
// 'includes')" and preventing the node from dialing its bootnodes on startup.
|
|
151
|
+
this.transports = libp2p.services.components.transportManager
|
|
152
|
+
.getTransports()
|
|
153
|
+
.map((t) => t[Symbol.toStringTag])
|
|
154
|
+
.map((tag) => {
|
|
155
|
+
if (tag?.includes("tcp")) return "tcp";
|
|
156
|
+
if (tag?.includes("quic")) return "quic";
|
|
157
|
+
return tag;
|
|
158
|
+
});
|
|
159
|
+
|
|
144
160
|
this.libp2p.addEventListener("peer:discovery", this.onDiscoveredPeer);
|
|
145
161
|
this.discv5.on("discovered", this.onDiscoveredENR);
|
|
146
162
|
|
|
@@ -183,17 +199,6 @@ export class PeerDiscovery {
|
|
|
183
199
|
}
|
|
184
200
|
});
|
|
185
201
|
}
|
|
186
|
-
|
|
187
|
-
// Transport tags vary by library: @libp2p/tcp uses '@libp2p/tcp', @chainsafe/libp2p-quic uses 'quic'
|
|
188
|
-
// Normalize to simple 'tcp' / 'quic' strings for matching
|
|
189
|
-
this.transports = libp2p.services.components.transportManager
|
|
190
|
-
.getTransports()
|
|
191
|
-
.map((t) => t[Symbol.toStringTag])
|
|
192
|
-
.map((tag) => {
|
|
193
|
-
if (tag?.includes("tcp")) return "tcp";
|
|
194
|
-
if (tag?.includes("quic")) return "quic";
|
|
195
|
-
return tag;
|
|
196
|
-
});
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
static async init(modules: PeerDiscoveryModules, opts: PeerDiscoveryOpts): Promise<PeerDiscovery> {
|
|
@@ -165,6 +165,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
165
165
|
// tracked in https://github.com/ChainSafe/lodestar/issues/7957
|
|
166
166
|
|
|
167
167
|
const logCtx = {
|
|
168
|
+
slot,
|
|
168
169
|
currentSlot: chain.clock.currentSlot,
|
|
169
170
|
peerId: peerIdStr,
|
|
170
171
|
delaySec,
|
|
@@ -184,20 +185,24 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
184
185
|
seenTimestampSec,
|
|
185
186
|
peerIdStr,
|
|
186
187
|
});
|
|
188
|
+
|
|
189
|
+
// Optimistically seed the payload-envelope cache too, mirroring seenBlockInputCache above.
|
|
190
|
+
// This ensures we have PayloadEnvelopeInput, even through "PARENT_UNKNOWN" error
|
|
191
|
+
// see https://github.com/ChainSafe/lodestar/issues/9475
|
|
192
|
+
if (isForkPostGloas(fork)) {
|
|
193
|
+
chain.seenPayloadEnvelopeInputCache.add({
|
|
194
|
+
blockRootHex,
|
|
195
|
+
block: signedBlock as SignedBeaconBlock<ForkPostGloas>,
|
|
196
|
+
forkName: fork,
|
|
197
|
+
sampledColumns: chain.custodyConfig.sampledColumns,
|
|
198
|
+
custodyColumns: chain.custodyConfig.custodyColumns,
|
|
199
|
+
timeCreatedSec: seenTimestampSec,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
187
203
|
try {
|
|
188
204
|
const {skippedSlots} = await validateGossipBlock(config, chain, signedBlock, fork);
|
|
189
205
|
|
|
190
|
-
if (isForkPostGloas(fork)) {
|
|
191
|
-
chain.seenPayloadEnvelopeInputCache.add({
|
|
192
|
-
blockRootHex,
|
|
193
|
-
block: signedBlock as SignedBeaconBlock<ForkPostGloas>,
|
|
194
|
-
forkName: fork,
|
|
195
|
-
sampledColumns: chain.custodyConfig.sampledColumns,
|
|
196
|
-
custodyColumns: chain.custodyConfig.custodyColumns,
|
|
197
|
-
timeCreatedSec: seenTimestampSec,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
206
|
const blockInputMeta = blockInput.getLogMeta();
|
|
202
207
|
|
|
203
208
|
const recvToValidation = Date.now() / 1000 - seenTimestampSec;
|
|
@@ -234,12 +239,22 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
234
239
|
throw e;
|
|
235
240
|
}
|
|
236
241
|
|
|
237
|
-
|
|
238
|
-
|
|
242
|
+
// IGNORE means the block is acceptable (e.g. FUTURE_SLOT, ALREADY_KNOWN), just not propagated.
|
|
243
|
+
// Keep the optimistically-added cache entries; they are pruned on finalization. Only REJECT
|
|
244
|
+
// (provably invalid) and unexpected errors prune below.
|
|
245
|
+
if (e.action === GossipAction.IGNORE) {
|
|
246
|
+
throw e;
|
|
239
247
|
}
|
|
248
|
+
|
|
249
|
+
chain.persistInvalidSszValue(forkTypes.SignedBeaconBlock, signedBlock, `gossip_reject_slot_${slot}`);
|
|
240
250
|
}
|
|
241
251
|
|
|
252
|
+
// REJECT or unexpected (non-BlockGossipError) error: drop the optimistically-added entries from
|
|
253
|
+
// both caches, keeping them consistent.
|
|
242
254
|
chain.seenBlockInputCache.prune(blockRootHex);
|
|
255
|
+
if (isForkPostGloas(fork)) {
|
|
256
|
+
chain.seenPayloadEnvelopeInputCache.prune(blockRootHex);
|
|
257
|
+
}
|
|
243
258
|
throw e;
|
|
244
259
|
}
|
|
245
260
|
}
|
|
@@ -1127,9 +1142,9 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1127
1142
|
const delaySec = chain.clock.secFromSlot(slot, seenTimestampSec);
|
|
1128
1143
|
|
|
1129
1144
|
logger.debug("Received gossip payload envelope", {
|
|
1145
|
+
slot,
|
|
1130
1146
|
currentSlot: chain.clock.currentSlot,
|
|
1131
1147
|
peerId: peerIdStr,
|
|
1132
|
-
slot,
|
|
1133
1148
|
blockRoot: toRootHex(envelope.beaconBlockRoot),
|
|
1134
1149
|
delaySec,
|
|
1135
1150
|
});
|
|
@@ -142,6 +142,7 @@ export function cacheByRangeResponses({
|
|
|
142
142
|
const blockRootHex = toRootHex(blockRoot);
|
|
143
143
|
|
|
144
144
|
const existing = updatedBatchBlocks.get(block.message.slot);
|
|
145
|
+
let blockInput: IBlockInput;
|
|
145
146
|
if (existing) {
|
|
146
147
|
// In practice this code block shouldn't be reached because we shouldn't be refetching a block we already have, see Batch#getRequests.
|
|
147
148
|
// Will throw if root hex does not match (meaning we are following the wrong chain)
|
|
@@ -155,8 +156,9 @@ export function cacheByRangeResponses({
|
|
|
155
156
|
},
|
|
156
157
|
{throwOnDuplicateAdd: false}
|
|
157
158
|
);
|
|
159
|
+
blockInput = existing;
|
|
158
160
|
} else {
|
|
159
|
-
|
|
161
|
+
blockInput = cache.getByBlock({
|
|
160
162
|
block,
|
|
161
163
|
blockRootHex,
|
|
162
164
|
source,
|
|
@@ -165,6 +167,21 @@ export function cacheByRangeResponses({
|
|
|
165
167
|
});
|
|
166
168
|
updatedBatchBlocks.set(blockInput.slot, blockInput);
|
|
167
169
|
}
|
|
170
|
+
|
|
171
|
+
// Seed seenPayloadEnvelopeInputCache the instant we have the block, before the blob loop (or any
|
|
172
|
+
// later step) can throw and abort the batch — otherwise a gloas block would sit in
|
|
173
|
+
// seenBlockInputCache but unseeded here, and payload-by-root sync would later throw "Missing
|
|
174
|
+
// PayloadEnvelopeInput for known block" (see issue #9306). add() is idempotent.
|
|
175
|
+
if (isForkPostGloas(blockInput.forkName)) {
|
|
176
|
+
seenPayloadEnvelopeInputCache.add({
|
|
177
|
+
blockRootHex: blockInput.blockRootHex,
|
|
178
|
+
block: blockInput.getBlock() as SignedBeaconBlock<ForkPostGloas>,
|
|
179
|
+
forkName: blockInput.forkName,
|
|
180
|
+
sampledColumns: custodyConfig.sampledColumns,
|
|
181
|
+
custodyColumns: custodyConfig.custodyColumns,
|
|
182
|
+
timeCreatedSec: seenTimestampSec,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
for (const {blockRoot, blobSidecars} of responses.validatedBlobSidecars ?? []) {
|
|
@@ -205,22 +222,6 @@ export function cacheByRangeResponses({
|
|
|
205
222
|
}
|
|
206
223
|
}
|
|
207
224
|
|
|
208
|
-
// Seed seenPayloadEnvelopeInputCache for every gloas block in the batch, regardless of whether
|
|
209
|
-
// the peer returned its envelope. Without this, a block returned without its envelope would be
|
|
210
|
-
// imported with no cache entry, and later payload-by-root sync would throw
|
|
211
|
-
// "Missing PayloadEnvelopeInput for known block" (see issue #9306).
|
|
212
|
-
for (const blockInput of updatedBatchBlocks.values()) {
|
|
213
|
-
if (!blockInput.hasBlock() || !isForkPostGloas(blockInput.forkName)) continue;
|
|
214
|
-
seenPayloadEnvelopeInputCache.add({
|
|
215
|
-
blockRootHex: blockInput.blockRootHex,
|
|
216
|
-
block: blockInput.getBlock() as SignedBeaconBlock<ForkPostGloas>,
|
|
217
|
-
forkName: blockInput.forkName,
|
|
218
|
-
sampledColumns: custodyConfig.sampledColumns,
|
|
219
|
-
custodyColumns: custodyConfig.custodyColumns,
|
|
220
|
-
timeCreatedSec: seenTimestampSec,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
225
|
let payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null =
|
|
225
226
|
existingPayloadEnvelopes !== null ? new Map(existingPayloadEnvelopes) : null;
|
|
226
227
|
if (downloadedPayloadEnvelopes !== null) {
|
|
@@ -229,8 +230,8 @@ export function cacheByRangeResponses({
|
|
|
229
230
|
const envelopeBlockRootHex = toRootHex(envelope.message.beaconBlockRoot);
|
|
230
231
|
const payloadInput = seenPayloadEnvelopeInputCache.get(envelopeBlockRootHex);
|
|
231
232
|
if (payloadInput === undefined) {
|
|
232
|
-
// Unreachable given the loop above seeded an entry for every gloas block in
|
|
233
|
-
// for the parent block, it's populated at BeaconChain init
|
|
233
|
+
// Unreachable given the validatedBlocks loop above seeded an entry for every gloas block in
|
|
234
|
+
// the batch. for the parent block, it's populated at BeaconChain init
|
|
234
235
|
throw new Error(`Missing PayloadEnvelopeInput for block ${envelopeBlockRootHex}`);
|
|
235
236
|
}
|
|
236
237
|
|
|
@@ -980,6 +981,12 @@ export async function validateColumnsByRangeResponse(
|
|
|
980
981
|
missingIndices: prettyPrintIndices(request.columns),
|
|
981
982
|
})
|
|
982
983
|
);
|
|
984
|
+
// Gloas: the bid commits to blobs even for payloads that get orphaned, whose columns are
|
|
985
|
+
// pruned and unservable. Skip instead of truncating + re-requesting forever; DA is enforced
|
|
986
|
+
// on envelope processing. Pre-Gloas commitments are canonical, so keep strict.
|
|
987
|
+
if (isForkPostGloas(forkName)) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
983
990
|
break;
|
|
984
991
|
}
|
|
985
992
|
|