@lodestar/beacon-node 1.45.0-dev.3100972235 → 1.45.0-dev.31b495a1d4
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/pool/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/pool/index.js +2 -43
- package/lib/api/impl/beacon/pool/index.js.map +1 -1
- package/lib/api/impl/beacon/state/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/state/index.js +46 -3
- package/lib/api/impl/beacon/state/index.js.map +1 -1
- package/lib/api/impl/beacon/state/utils.d.ts +11 -2
- package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
- package/lib/api/impl/beacon/state/utils.js +39 -3
- package/lib/api/impl/beacon/state/utils.js.map +1 -1
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +46 -6
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/blocks/importBlock.js +1 -1
- 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/chain.d.ts +1 -2
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +1 -3
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/interface.d.ts +1 -2
- package/lib/chain/interface.d.ts.map +1 -1
- package/lib/chain/interface.js.map +1 -1
- package/lib/chain/opPools/proposerPreferencesPool.d.ts +2 -1
- package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
- package/lib/chain/opPools/proposerPreferencesPool.js +3 -0
- package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
- package/lib/chain/options.d.ts +1 -0
- package/lib/chain/options.d.ts.map +1 -1
- package/lib/chain/options.js +1 -0
- package/lib/chain/options.js.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +4 -12
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/seenCache/index.d.ts +0 -1
- package/lib/chain/seenCache/index.d.ts.map +1 -1
- package/lib/chain/seenCache/index.js +0 -1
- package/lib/chain/seenCache/index.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/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
- package/lib/chain/stateCache/persistentCheckpointsCache.js +3 -2
- package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
- package/lib/chain/validation/executionPayloadBid.js +11 -10
- package/lib/chain/validation/executionPayloadBid.js.map +1 -1
- package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
- package/lib/chain/validation/proposerPreferences.js +14 -5
- package/lib/chain/validation/proposerPreferences.js.map +1 -1
- package/lib/network/core/metrics.d.ts +65 -0
- package/lib/network/core/metrics.d.ts.map +1 -1
- package/lib/network/core/metrics.js +94 -0
- package/lib/network/core/metrics.js.map +1 -1
- package/lib/network/discv5/utils.d.ts.map +1 -1
- package/lib/network/discv5/utils.js +12 -3
- package/lib/network/discv5/utils.js.map +1 -1
- package/lib/network/gossip/encoding.d.ts.map +1 -1
- package/lib/network/gossip/encoding.js +7 -0
- package/lib/network/gossip/encoding.js.map +1 -1
- package/lib/network/gossip/gossipsub.d.ts.map +1 -1
- package/lib/network/gossip/gossipsub.js +2 -1
- package/lib/network/gossip/gossipsub.js.map +1 -1
- package/lib/network/gossip/topic.d.ts +6 -1
- package/lib/network/gossip/topic.d.ts.map +1 -1
- package/lib/network/gossip/topic.js +23 -3
- package/lib/network/gossip/topic.js.map +1 -1
- package/lib/network/peers/peerManager.d.ts.map +1 -1
- package/lib/network/peers/peerManager.js +10 -0
- package/lib/network/peers/peerManager.js.map +1 -1
- package/lib/network/peers/score/score.d.ts +3 -1
- package/lib/network/peers/score/score.d.ts.map +1 -1
- package/lib/network/peers/score/score.js +6 -1
- package/lib/network/peers/score/score.js.map +1 -1
- package/lib/network/peers/score/store.d.ts.map +1 -1
- package/lib/network/peers/score/store.js +6 -1
- package/lib/network/peers/score/store.js.map +1 -1
- package/lib/network/peers/utils/prioritizePeers.d.ts.map +1 -1
- package/lib/network/peers/utils/prioritizePeers.js +14 -0
- package/lib/network/peers/utils/prioritizePeers.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +28 -16
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/sync/range/chain.d.ts +4 -1
- package/lib/sync/range/chain.d.ts.map +1 -1
- package/lib/sync/range/chain.js +3 -3
- 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 +10 -0
- package/lib/sync/range/range.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/lib/util/graffiti.d.ts +15 -0
- package/lib/util/graffiti.d.ts.map +1 -1
- package/lib/util/graffiti.js +55 -0
- package/lib/util/graffiti.js.map +1 -1
- package/package.json +14 -14
- package/src/api/impl/beacon/pool/index.ts +0 -53
- package/src/api/impl/beacon/state/index.ts +52 -1
- package/src/api/impl/beacon/state/utils.ts +57 -4
- package/src/api/impl/validator/index.ts +54 -5
- package/src/chain/blocks/importBlock.ts +1 -1
- package/src/chain/blocks/payloadEnvelopeProcessor.ts +17 -17
- package/src/chain/chain.ts +0 -3
- package/src/chain/interface.ts +0 -2
- package/src/chain/opPools/proposerPreferencesPool.ts +5 -1
- package/src/chain/options.ts +2 -0
- package/src/chain/produceBlock/produceBlockBody.ts +9 -17
- package/src/chain/seenCache/index.ts +0 -1
- package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +8 -0
- package/src/chain/stateCache/persistentCheckpointsCache.ts +3 -2
- package/src/chain/validation/executionPayloadBid.ts +14 -9
- package/src/chain/validation/proposerPreferences.ts +15 -5
- package/src/network/core/metrics.ts +94 -0
- package/src/network/discv5/utils.ts +11 -3
- package/src/network/gossip/encoding.ts +7 -0
- package/src/network/gossip/gossipsub.ts +2 -1
- package/src/network/gossip/topic.ts +28 -4
- package/src/network/peers/peerManager.ts +11 -0
- package/src/network/peers/score/score.ts +8 -1
- package/src/network/peers/score/store.ts +8 -1
- package/src/network/peers/utils/prioritizePeers.ts +15 -0
- package/src/network/processor/gossipHandlers.ts +30 -16
- package/src/sync/range/chain.ts +4 -5
- package/src/sync/range/range.ts +10 -0
- package/src/sync/utils/downloadByRange.ts +26 -19
- package/src/util/graffiti.ts +78 -0
- package/lib/chain/seenCache/seenProposerPreferences.d.ts +0 -16
- package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +0 -1
- package/lib/chain/seenCache/seenProposerPreferences.js +0 -26
- package/lib/chain/seenCache/seenProposerPreferences.js.map +0 -1
- package/src/chain/seenCache/seenProposerPreferences.ts +0 -32
|
@@ -127,6 +127,100 @@ export function createNetworkCoreMetrics(register: RegistryMetricCreator) {
|
|
|
127
127
|
name: "lodestar_peer_manager_starved_bool",
|
|
128
128
|
help: "Whether lodestar is starved of data while syncing",
|
|
129
129
|
}),
|
|
130
|
+
/**
|
|
131
|
+
* Core selection/pruning phase of the heartbeat, split out of heartbeatDuration so it
|
|
132
|
+
* can be attributed on its own. Divide by peersEvaluated for time-per-peer.
|
|
133
|
+
*/
|
|
134
|
+
prioritizePeersDuration: register.histogram({
|
|
135
|
+
name: "lodestar_peer_manager_prioritize_peers_seconds",
|
|
136
|
+
help: "prioritizePeers function duration in seconds, the core peer selection/pruning algorithm",
|
|
137
|
+
buckets: [0.0005, 0.001, 0.005, 0.01, 0.05, 0.1],
|
|
138
|
+
}),
|
|
139
|
+
/**
|
|
140
|
+
* Score decay + map prune over every tracked peer, split out of heartbeatDuration.
|
|
141
|
+
* This loop iterates the whole score store, so divide by scoreMapSize (not
|
|
142
|
+
* peersEvaluated) for time-per-peer.
|
|
143
|
+
*/
|
|
144
|
+
scoreUpdateDuration: register.histogram({
|
|
145
|
+
name: "lodestar_peer_score_update_seconds",
|
|
146
|
+
help: "Peer score store update (decay + prune over all peers) duration in seconds",
|
|
147
|
+
buckets: [0.0005, 0.001, 0.005, 0.01, 0.05, 0.1],
|
|
148
|
+
}),
|
|
149
|
+
/**
|
|
150
|
+
* Denominator for prioritizePeersDuration: it scales with the number of connected
|
|
151
|
+
* healthy peers, so time-per-peer (prioritizePeersDuration / this) is the comparable
|
|
152
|
+
* figure. For scoreUpdateDuration use scoreMapSize instead.
|
|
153
|
+
*/
|
|
154
|
+
peersEvaluated: register.histogram({
|
|
155
|
+
name: "lodestar_peer_manager_peers_evaluated_count",
|
|
156
|
+
help: "Number of connected healthy peers evaluated by prioritizePeers per heartbeat, denominator for prioritize_peers_seconds",
|
|
157
|
+
buckets: [0, 25, 50, 75, 100, 150, 200],
|
|
158
|
+
}),
|
|
159
|
+
/**
|
|
160
|
+
* Every peer the manager intends to disconnect. Covers both the top-of-heartbeat
|
|
161
|
+
* bad-score disconnects ("banned"/"score_too_low") and the prioritization reasons;
|
|
162
|
+
* the older peersRequestedToDisconnect gauge only recorded the latter. Counts intent,
|
|
163
|
+
* not completion (goodbye is fire-and-forget).
|
|
164
|
+
*/
|
|
165
|
+
peersPruned: register.counter<{reason: string}>({
|
|
166
|
+
name: "lodestar_peer_manager_peers_pruned_total",
|
|
167
|
+
help: "Total peers the peer manager intends to disconnect, labeled by reason (incl. bad-score and prioritization reasons)",
|
|
168
|
+
labelNames: ["reason"],
|
|
169
|
+
}),
|
|
170
|
+
/**
|
|
171
|
+
* Actual peer count per active subnet, to check the min-peers-per-subnet invariant.
|
|
172
|
+
* Buckets straddle the target (TARGET_SUBNET_PEERS = 6).
|
|
173
|
+
*/
|
|
174
|
+
peersPerActiveSubnet: register.histogram<{type: SubnetType}>({
|
|
175
|
+
name: "lodestar_peer_manager_peers_per_active_subnet",
|
|
176
|
+
help: "Histogram of connected peer count per active subnet, labeled by subnet type",
|
|
177
|
+
labelNames: ["type"],
|
|
178
|
+
buckets: [0, 2, 4, 6, 8, 12],
|
|
179
|
+
}),
|
|
180
|
+
/**
|
|
181
|
+
* Live outbound ratio, to check the OUTBOUND_PEERS_RATIO (10%) invariant (issue #2215).
|
|
182
|
+
* Denominator is connected healthy peers (banned/disconnected already removed).
|
|
183
|
+
*/
|
|
184
|
+
outboundPeersRatio: register.gauge({
|
|
185
|
+
name: "lodestar_peer_manager_outbound_peers_ratio",
|
|
186
|
+
help: "Ratio of outbound peers to total connected healthy peers, verifies the outbound peers invariant",
|
|
187
|
+
}),
|
|
188
|
+
/**
|
|
189
|
+
* Score state crossings (Healthy/Disconnected/Banned). Sensitive to the decay formula,
|
|
190
|
+
* thresholds, and gossip-score weighting.
|
|
191
|
+
*/
|
|
192
|
+
scoreStateTransitions: register.counter<{from: string; to: string}>({
|
|
193
|
+
name: "lodestar_peer_score_state_transitions_total",
|
|
194
|
+
help: "Total peer score state transitions, labeled by from and to state (Healthy/Disconnected/Banned)",
|
|
195
|
+
labelNames: ["from", "to"],
|
|
196
|
+
}),
|
|
197
|
+
/**
|
|
198
|
+
* Entry count of the score store (not bytes). Reflects the prune-to-MAX_ENTRIES /
|
|
199
|
+
* SCORE_THRESHOLD retention logic. Also the denominator for scoreUpdateDuration
|
|
200
|
+
* time-per-peer (that loop iterates the whole store, not just connected peers).
|
|
201
|
+
*/
|
|
202
|
+
scoreMapSize: register.gauge({
|
|
203
|
+
name: "lodestar_peer_manager_score_map_size",
|
|
204
|
+
help: "Current number of entries in the peer score store",
|
|
205
|
+
}),
|
|
206
|
+
/**
|
|
207
|
+
* Entry count of the connectedPeers map (not bytes). Should track libp2p_peers; a
|
|
208
|
+
* persistent gap indicates the connection leak guarded by leakedConnectionsCount.
|
|
209
|
+
*/
|
|
210
|
+
connectedPeersMapSize: register.gauge({
|
|
211
|
+
name: "lodestar_peer_manager_connected_peers_map_size",
|
|
212
|
+
help: "Current number of entries in the peer manager connectedPeers map",
|
|
213
|
+
}),
|
|
214
|
+
/**
|
|
215
|
+
* assertPeerRelevance outcome per Status exchange: "relevant", an irrelevant reason
|
|
216
|
+
* code, or "error". Fires on every status evaluation (inbound requests and our own
|
|
217
|
+
* requestStatus responses both route through onStatus).
|
|
218
|
+
*/
|
|
219
|
+
relevanceCheck: register.counter<{result: string}>({
|
|
220
|
+
name: "lodestar_peer_relevance_check_total",
|
|
221
|
+
help: "Total peer relevance checks on Status, labeled by result (relevant or irrelevant reason code)",
|
|
222
|
+
labelNames: ["result"],
|
|
223
|
+
}),
|
|
130
224
|
},
|
|
131
225
|
leakedConnectionsCount: register.gauge({
|
|
132
226
|
name: "lodestar_peer_manager_leaked_connections_count",
|
|
@@ -13,9 +13,17 @@ export enum ENRRelevance {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function enrRelevance(enr: ENR, config: BeaconConfig, clock: IClock): ENRRelevance {
|
|
16
|
-
// We are not interested in peers that don't advertise at least one transport (tcp or quic)
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// We are not interested in peers that don't advertise at least one transport (tcp or quic).
|
|
17
|
+
let multiaddrTCP: ReturnType<ENR["getLocationMultiaddr"]>;
|
|
18
|
+
let multiaddrQUIC: ReturnType<ENR["getLocationMultiaddr"]>;
|
|
19
|
+
// Malformed ENR endpoint fields can make multiaddr conversion throw.
|
|
20
|
+
// Treat endpoints that cannot be converted as unusable.
|
|
21
|
+
try {
|
|
22
|
+
multiaddrTCP = enr.getLocationMultiaddr(ENRKey.tcp);
|
|
23
|
+
multiaddrQUIC = enr.getLocationMultiaddr(ENRKey.quic);
|
|
24
|
+
} catch {
|
|
25
|
+
return ENRRelevance.no_transport;
|
|
26
|
+
}
|
|
19
27
|
if (!multiaddrTCP && !multiaddrQUIC) {
|
|
20
28
|
return ENRRelevance.no_transport;
|
|
21
29
|
}
|
|
@@ -52,6 +52,13 @@ export function fastMsgIdFn(rpcMsg: RPC.Message): string {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export function msgIdToStrFn(msgId: Uint8Array): string {
|
|
55
|
+
// Spec mandates a 20-byte gossipsub message-id, but control-message IDs from peers
|
|
56
|
+
// can be any length. Reject non-20-byte IDs to avoid a Buffer.set() RangeError on
|
|
57
|
+
// longer IDs and stale shared-buffer bytes on shorter ones.
|
|
58
|
+
// Ref: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.10/specs/phase0/p2p-interface.md?plain=1#L486
|
|
59
|
+
if (msgId.length !== 20) {
|
|
60
|
+
throw Error(`Expect msgId to be 20 bytes, got ${msgId.length}`);
|
|
61
|
+
}
|
|
55
62
|
// this is the same logic to `toHex(msgId)` with better performance
|
|
56
63
|
sharedMsgIdBuf.set(msgId);
|
|
57
64
|
return `0x${sharedMsgIdBuf.toString("hex")}`;
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
computeGossipPeerScoreParams,
|
|
36
36
|
gossipScoreThresholds,
|
|
37
37
|
} from "./scoringParameters.js";
|
|
38
|
-
import {GossipTopicCache, getCoreTopicsAtFork, stringifyGossipTopic} from "./topic.js";
|
|
38
|
+
import {GossipTopicCache, getAllowedTopics, getCoreTopicsAtFork, stringifyGossipTopic} from "./topic.js";
|
|
39
39
|
|
|
40
40
|
/** As specified in https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/p2p-interface.md */
|
|
41
41
|
const GOSSIPSUB_HEARTBEAT_INTERVAL = 0.7 * 1000;
|
|
@@ -140,6 +140,7 @@ export class Eth2Gossipsub {
|
|
|
140
140
|
const gossipsubInstance = gossipsub({
|
|
141
141
|
globalSignaturePolicy: StrictNoSign,
|
|
142
142
|
allowPublishToZeroTopicPeers: allowPublishToZeroPeers,
|
|
143
|
+
allowedTopics: getAllowedTopics(networkConfig),
|
|
143
144
|
D: gossipsubD ?? GOSSIP_D,
|
|
144
145
|
Dlo: gossipsubDLow ?? GOSSIP_D_LOW,
|
|
145
146
|
Dhi: gossipsubDHigh ?? GOSSIP_D_HIGH,
|
|
@@ -246,7 +246,7 @@ export function parseGossipTopic(forkDigestContext: ForkDigestContext, topicStr:
|
|
|
246
246
|
export function getCoreTopicsAtFork(
|
|
247
247
|
networkConfig: NetworkConfig,
|
|
248
248
|
fork: ForkName,
|
|
249
|
-
opts: {subscribeAllSubnets?: boolean; disableLightClientServer?: boolean}
|
|
249
|
+
opts: {subscribeAllSubnets?: boolean; subscribeAllColumnSubnets?: boolean; disableLightClientServer?: boolean}
|
|
250
250
|
): GossipTopicTypeMap[keyof GossipTopicTypeMap][] {
|
|
251
251
|
// Common topics for all forks
|
|
252
252
|
const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = [
|
|
@@ -266,7 +266,7 @@ export function getCoreTopicsAtFork(
|
|
|
266
266
|
|
|
267
267
|
// After fulu also track data_column_sidecar_{index}
|
|
268
268
|
if (ForkSeq[fork] >= ForkSeq.fulu) {
|
|
269
|
-
topics.push(...getDataColumnSidecarTopics(networkConfig));
|
|
269
|
+
topics.push(...getDataColumnSidecarTopics(networkConfig, opts.subscribeAllColumnSubnets));
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
// After Deneb and before Fulu also track blob_sidecar_{subnet_id}
|
|
@@ -309,15 +309,39 @@ export function getCoreTopicsAtFork(
|
|
|
309
309
|
return topics;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Build the complete set of valid gossip topic strings across every fork boundary.
|
|
314
|
+
*/
|
|
315
|
+
export function getAllowedTopics(networkConfig: NetworkConfig): Set<string> {
|
|
316
|
+
const {config} = networkConfig;
|
|
317
|
+
const allowedTopics = new Set<string>();
|
|
318
|
+
|
|
319
|
+
for (const boundary of config.forkBoundariesAscendingEpochOrder) {
|
|
320
|
+
const topics = getCoreTopicsAtFork(networkConfig, boundary.fork, {
|
|
321
|
+
subscribeAllSubnets: true,
|
|
322
|
+
subscribeAllColumnSubnets: true,
|
|
323
|
+
disableLightClientServer: false,
|
|
324
|
+
});
|
|
325
|
+
for (const topic of topics) {
|
|
326
|
+
allowedTopics.add(stringifyGossipTopic(config, {...topic, boundary}));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return allowedTopics;
|
|
331
|
+
}
|
|
332
|
+
|
|
312
333
|
/**
|
|
313
334
|
* Pick data column subnets to subscribe to post-fulu.
|
|
314
335
|
*/
|
|
315
336
|
export function getDataColumnSidecarTopics(
|
|
316
|
-
networkConfig: NetworkConfig
|
|
337
|
+
networkConfig: NetworkConfig,
|
|
338
|
+
subscribeAllColumnSubnets = false
|
|
317
339
|
): GossipTopicTypeMap[keyof GossipTopicTypeMap][] {
|
|
318
340
|
const topics: GossipTopicTypeMap[keyof GossipTopicTypeMap][] = [];
|
|
319
341
|
|
|
320
|
-
const subnets =
|
|
342
|
+
const subnets = subscribeAllColumnSubnets
|
|
343
|
+
? Array.from({length: networkConfig.config.DATA_COLUMN_SIDECAR_SUBNET_COUNT}, (_, i) => i)
|
|
344
|
+
: networkConfig.custodyConfig.sampledSubnets;
|
|
321
345
|
for (const subnet of subnets) {
|
|
322
346
|
topics.push({type: GossipType.data_column_sidecar, subnet});
|
|
323
347
|
}
|
|
@@ -415,8 +415,10 @@ export class PeerManager {
|
|
|
415
415
|
);
|
|
416
416
|
if (irrelevantReasonType === null) {
|
|
417
417
|
isIrrelevant = false;
|
|
418
|
+
this.metrics?.peerManager.relevanceCheck.inc({result: "relevant"});
|
|
418
419
|
} else {
|
|
419
420
|
isIrrelevant = true;
|
|
421
|
+
this.metrics?.peerManager.relevanceCheck.inc({result: irrelevantReasonType.code});
|
|
420
422
|
this.logger.debug("Irrelevant peer", {
|
|
421
423
|
peer: prettyPrintPeerId(peer),
|
|
422
424
|
reason: renderIrrelevantPeerType(irrelevantReasonType),
|
|
@@ -424,6 +426,7 @@ export class PeerManager {
|
|
|
424
426
|
}
|
|
425
427
|
} catch (e) {
|
|
426
428
|
this.logger.error("Irrelevant peer - unexpected error", {peer: prettyPrintPeerId(peer)}, e as Error);
|
|
429
|
+
this.metrics?.peerManager.relevanceCheck.inc({result: "error"});
|
|
427
430
|
isIrrelevant = true;
|
|
428
431
|
}
|
|
429
432
|
|
|
@@ -552,15 +555,18 @@ export class PeerManager {
|
|
|
552
555
|
for (const peer of connectedPeers) {
|
|
553
556
|
switch (this.peerRpcScores.getScoreState(peer)) {
|
|
554
557
|
case ScoreState.Banned:
|
|
558
|
+
this.metrics?.peerManager.peersPruned.inc({reason: "banned"});
|
|
555
559
|
void this.goodbyeAndDisconnect(peer, GoodByeReasonCode.BANNED);
|
|
556
560
|
break;
|
|
557
561
|
case ScoreState.Disconnected:
|
|
562
|
+
this.metrics?.peerManager.peersPruned.inc({reason: "score_too_low"});
|
|
558
563
|
void this.goodbyeAndDisconnect(peer, GoodByeReasonCode.SCORE_TOO_LOW);
|
|
559
564
|
break;
|
|
560
565
|
case ScoreState.Healthy:
|
|
561
566
|
connectedHealthyPeers.push(peer);
|
|
562
567
|
}
|
|
563
568
|
}
|
|
569
|
+
this.metrics?.peerManager.peersEvaluated.observe(connectedHealthyPeers.length);
|
|
564
570
|
|
|
565
571
|
const status = this.statusCache.get();
|
|
566
572
|
const starved =
|
|
@@ -572,6 +578,7 @@ export class PeerManager {
|
|
|
572
578
|
this.metrics?.peerManager.starved.set(starved ? 1 : 0);
|
|
573
579
|
const forkSeq = this.config.getForkSeq(this.clock.currentSlot);
|
|
574
580
|
|
|
581
|
+
const prioritizeTimer = this.metrics?.peerManager.prioritizePeersDuration.startTimer();
|
|
575
582
|
const {peersToDisconnect, peersToConnect, attnetQueries, syncnetQueries, custodyGroupQueries} = prioritizePeers(
|
|
576
583
|
connectedHealthyPeers.map((peer) => {
|
|
577
584
|
const peerData = this.connectedPeers.get(peer.toString());
|
|
@@ -601,6 +608,7 @@ export class PeerManager {
|
|
|
601
608
|
this.config,
|
|
602
609
|
this.metrics
|
|
603
610
|
);
|
|
611
|
+
prioritizeTimer?.();
|
|
604
612
|
|
|
605
613
|
const queriesMerged: SubnetDiscvQueryMs[] = [];
|
|
606
614
|
for (const {type, queries} of [
|
|
@@ -632,6 +640,7 @@ export class PeerManager {
|
|
|
632
640
|
// disconnect first to have more slots before we dial new peers
|
|
633
641
|
for (const [reason, peers] of peersToDisconnect) {
|
|
634
642
|
this.metrics?.peersRequestedToDisconnect.inc({reason}, peers.length);
|
|
643
|
+
this.metrics?.peerManager.peersPruned.inc({reason}, peers.length);
|
|
635
644
|
for (const peer of peers) {
|
|
636
645
|
void this.goodbyeAndDisconnect(peer, GoodByeReasonCode.TOO_MANY_PEERS);
|
|
637
646
|
}
|
|
@@ -659,6 +668,8 @@ export class PeerManager {
|
|
|
659
668
|
}
|
|
660
669
|
}
|
|
661
670
|
|
|
671
|
+
this.metrics?.peerManager.connectedPeersMapSize.set(this.connectedPeers.size);
|
|
672
|
+
|
|
662
673
|
timer?.();
|
|
663
674
|
|
|
664
675
|
this.logger.debug("peerManager heartbeat result", {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {GoodByeReasonCode} from "../../../constants/network.js";
|
|
2
|
+
import {NetworkCoreMetrics} from "../../core/metrics.js";
|
|
2
3
|
import {
|
|
3
4
|
COOL_DOWN_BEFORE_DECAY_MS,
|
|
4
5
|
DEFAULT_SCORE,
|
|
@@ -23,13 +24,15 @@ export class RealScore implements IPeerScore {
|
|
|
23
24
|
/** The final score, computed from the above */
|
|
24
25
|
private score: number;
|
|
25
26
|
private lastUpdate: number;
|
|
27
|
+
private readonly metrics: NetworkCoreMetrics | null;
|
|
26
28
|
|
|
27
|
-
constructor() {
|
|
29
|
+
constructor(metrics: NetworkCoreMetrics | null = null) {
|
|
28
30
|
this.lodestarScore = DEFAULT_SCORE;
|
|
29
31
|
this.gossipScore = DEFAULT_SCORE;
|
|
30
32
|
this.score = DEFAULT_SCORE;
|
|
31
33
|
this.ignoreNegativeGossipScore = false;
|
|
32
34
|
this.lastUpdate = Date.now();
|
|
35
|
+
this.metrics = metrics;
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
isCoolingDown(): boolean {
|
|
@@ -136,6 +139,10 @@ export class RealScore implements IPeerScore {
|
|
|
136
139
|
this.recomputeScore();
|
|
137
140
|
const newState = scoreToState(this.score);
|
|
138
141
|
|
|
142
|
+
if (prevState !== newState) {
|
|
143
|
+
this.metrics?.peerManager.scoreStateTransitions.inc({from: prevState, to: newState});
|
|
144
|
+
}
|
|
145
|
+
|
|
139
146
|
if (prevState !== ScoreState.Banned && newState === ScoreState.Banned) {
|
|
140
147
|
// ban this peer for at least BANNED_BEFORE_DECAY_MS seconds
|
|
141
148
|
this.lastUpdate = Date.now() + COOL_DOWN_BEFORE_DECAY_MS;
|
|
@@ -32,7 +32,9 @@ export class PeerRpcScoreStore implements IPeerRpcScoreStore {
|
|
|
32
32
|
constructor(opts: PeerRpcScoreOpts = {}, metrics: NetworkCoreMetrics | null = null, logger: Logger | null = null) {
|
|
33
33
|
this.metrics = metrics;
|
|
34
34
|
this.logger = logger;
|
|
35
|
-
this.scores = opts.disablePeerScoring
|
|
35
|
+
this.scores = opts.disablePeerScoring
|
|
36
|
+
? new MapDef(() => new MaxScore())
|
|
37
|
+
: new MapDef(() => new RealScore(this.metrics));
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
getScore(peer: PeerId): number {
|
|
@@ -75,6 +77,8 @@ export class PeerRpcScoreStore implements IPeerRpcScoreStore {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
update(): void {
|
|
80
|
+
const timer = this.metrics?.peerManager.scoreUpdateDuration.startTimer();
|
|
81
|
+
|
|
78
82
|
// Bound size of data structures
|
|
79
83
|
pruneSetToMax(this.scores, MAX_ENTRIES);
|
|
80
84
|
|
|
@@ -86,6 +90,9 @@ export class PeerRpcScoreStore implements IPeerRpcScoreStore {
|
|
|
86
90
|
this.scores.delete(peerIdStr);
|
|
87
91
|
}
|
|
88
92
|
}
|
|
93
|
+
|
|
94
|
+
this.metrics?.peerManager.scoreMapSize.set(this.scores.size);
|
|
95
|
+
timer?.();
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
updateGossipsubScore(peerId: PeerIdStr, newScore: number, ignore: boolean): void {
|
|
@@ -7,6 +7,7 @@ import {MapDef} from "@lodestar/utils";
|
|
|
7
7
|
import {shuffle} from "../../../util/shuffle.js";
|
|
8
8
|
import {sortBy} from "../../../util/sortBy.js";
|
|
9
9
|
import {NetworkCoreMetrics} from "../../core/metrics.js";
|
|
10
|
+
import {SubnetType} from "../../metadata.js";
|
|
10
11
|
import {RequestedSubnet} from "./subnetMap.js";
|
|
11
12
|
|
|
12
13
|
/** Target number of peers we'd like to have connected to a given long-lived subnet */
|
|
@@ -189,6 +190,18 @@ export function prioritizePeers(
|
|
|
189
190
|
|
|
190
191
|
const connectedPeerCount = connectedPeers.length;
|
|
191
192
|
|
|
193
|
+
// Track the outbound peers invariant (we keep at least OUTBOUND_PEERS_RATIO outbound peers).
|
|
194
|
+
// Report 0 when there are no peers so the gauge doesn't go stale at its last value.
|
|
195
|
+
if (metrics) {
|
|
196
|
+
let outboundPeerCount = 0;
|
|
197
|
+
for (const peer of connectedPeers) {
|
|
198
|
+
if (peer.direction === "outbound") {
|
|
199
|
+
outboundPeerCount++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
metrics.peerManager.outboundPeersRatio.set(connectedPeerCount > 0 ? outboundPeerCount / connectedPeerCount : 0);
|
|
203
|
+
}
|
|
204
|
+
|
|
192
205
|
if (connectedPeerCount < targetPeers) {
|
|
193
206
|
// Need more peers.
|
|
194
207
|
// Instead of attempting to connect the exact amount necessary this will overshoot a little since the success
|
|
@@ -256,6 +269,7 @@ function requestSubnetPeers(
|
|
|
256
269
|
|
|
257
270
|
for (const {subnet, toSlot} of activeAttnets) {
|
|
258
271
|
const peersInSubnet = peersPerSubnet.get(subnet) ?? 0;
|
|
272
|
+
metrics?.peerManager.peersPerActiveSubnet.observe({type: SubnetType.attnets}, peersInSubnet);
|
|
259
273
|
if (peersInSubnet < targetSubnetPeers) {
|
|
260
274
|
// We need more peers
|
|
261
275
|
attnetQueries.push({subnet, toSlot, maxPeersToDiscover: targetSubnetPeers - peersInSubnet});
|
|
@@ -282,6 +296,7 @@ function requestSubnetPeers(
|
|
|
282
296
|
|
|
283
297
|
for (const {subnet, toSlot} of activeSyncnets) {
|
|
284
298
|
const peersInSubnet = peersPerSubnet.get(subnet) ?? 0;
|
|
299
|
+
metrics?.peerManager.peersPerActiveSubnet.observe({type: SubnetType.syncnets}, peersInSubnet);
|
|
285
300
|
if (peersInSubnet < targetSubnetPeers) {
|
|
286
301
|
// We need more peers
|
|
287
302
|
syncnetQueries.push({subnet, toSlot, maxPeersToDiscover: targetSubnetPeers - peersInSubnet});
|
|
@@ -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
|
});
|
|
@@ -1258,9 +1273,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1258
1273
|
const signedProposerPreferences = sszDeserialize(topic, serializedData);
|
|
1259
1274
|
await validateGossipProposerPreferences(chain, signedProposerPreferences);
|
|
1260
1275
|
|
|
1261
|
-
chain.proposerPreferencesPool.add(signedProposerPreferences);
|
|
1262
1276
|
chain.emitter.emit(routes.events.EventType.proposerPreferences, {
|
|
1263
|
-
version:
|
|
1277
|
+
version: config.getForkName(signedProposerPreferences.message.proposalSlot),
|
|
1264
1278
|
data: signedProposerPreferences,
|
|
1265
1279
|
});
|
|
1266
1280
|
},
|
package/src/sync/range/chain.ts
CHANGED
|
@@ -186,10 +186,6 @@ export class SyncChain {
|
|
|
186
186
|
this.logger = logger;
|
|
187
187
|
this.logId = `${syncType}-${nextChainId++}`;
|
|
188
188
|
|
|
189
|
-
if (metrics) {
|
|
190
|
-
metrics.syncRange.headSyncPeers.addCollect(() => this.scrapeMetrics(metrics));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
189
|
// Trigger event on parent class
|
|
194
190
|
this.sync().then(
|
|
195
191
|
() => fns.onEnd(null, this.target),
|
|
@@ -775,7 +771,10 @@ export class SyncChain {
|
|
|
775
771
|
});
|
|
776
772
|
}
|
|
777
773
|
|
|
778
|
-
|
|
774
|
+
/**
|
|
775
|
+
* Called by `RangeSync`'s to avoid collecting metrics of removed chains.
|
|
776
|
+
*/
|
|
777
|
+
scrapeMetrics(metrics: Metrics): void {
|
|
779
778
|
const syncPeersMetric =
|
|
780
779
|
this.syncType === RangeSyncType.Finalized
|
|
781
780
|
? metrics.syncRange.finalizedSyncPeers
|
package/src/sync/range/range.ts
CHANGED
|
@@ -105,6 +105,16 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
105
105
|
|
|
106
106
|
if (metrics) {
|
|
107
107
|
metrics.syncStatus.addCollect(() => this.scrapeMetrics(metrics));
|
|
108
|
+
metrics.syncRange.headSyncPeers.addCollect(() => {
|
|
109
|
+
// Gauges retain their last set value, so a removed chain would keep reporting stale
|
|
110
|
+
// per-column peer counts (finalizedSyncPeers especially, as it is rarely recreated once
|
|
111
|
+
// synced)
|
|
112
|
+
metrics.syncRange.headSyncPeers.reset();
|
|
113
|
+
metrics.syncRange.finalizedSyncPeers.reset();
|
|
114
|
+
for (const syncChain of this.chains.values()) {
|
|
115
|
+
syncChain.scrapeMetrics(metrics);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
108
118
|
}
|
|
109
119
|
}
|
|
110
120
|
|
|
@@ -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
|
|