@lodestar/beacon-node 1.47.0-dev.b2cbfa50c2 → 1.47.0-dev.be9d5b774d
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/blocks/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/blocks/index.js +3 -6
- package/lib/api/impl/beacon/blocks/index.js.map +1 -1
- package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/pool/index.js +4 -0
- package/lib/api/impl/beacon/pool/index.js.map +1 -1
- package/lib/api/impl/events/index.d.ts +1 -1
- package/lib/api/impl/events/index.d.ts.map +1 -1
- package/lib/api/impl/events/index.js +10 -3
- package/lib/api/impl/events/index.js.map +1 -1
- package/lib/api/impl/validator/index.js +1 -1
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/archiveStore/historicalState/metrics.d.ts.map +1 -1
- package/lib/chain/archiveStore/historicalState/metrics.js +21 -0
- package/lib/chain/archiveStore/historicalState/metrics.js.map +1 -1
- package/lib/chain/blocks/importBlock.d.ts.map +1 -1
- package/lib/chain/blocks/importBlock.js +2 -7
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/builderCircuitBreaker.d.ts +6 -5
- package/lib/chain/builderCircuitBreaker.d.ts.map +1 -1
- package/lib/chain/builderCircuitBreaker.js +28 -15
- package/lib/chain/builderCircuitBreaker.js.map +1 -1
- package/lib/chain/errors/proposerPreferences.d.ts +6 -0
- package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
- package/lib/chain/errors/proposerPreferences.js +1 -0
- package/lib/chain/errors/proposerPreferences.js.map +1 -1
- package/lib/chain/options.d.ts +1 -1
- package/lib/chain/options.d.ts.map +1 -1
- package/lib/chain/prepareNextSlot.js +1 -1
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/validation/proposerPreferences.d.ts +1 -1
- package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
- package/lib/chain/validation/proposerPreferences.js +11 -2
- package/lib/chain/validation/proposerPreferences.js.map +1 -1
- package/lib/execution/builder/http.d.ts +10 -7
- package/lib/execution/builder/http.d.ts.map +1 -1
- package/lib/execution/builder/http.js +11 -8
- package/lib/execution/builder/http.js.map +1 -1
- package/lib/metrics/metrics/beacon.d.ts +3 -3
- package/lib/metrics/metrics/beacon.d.ts.map +1 -1
- package/lib/metrics/metrics/beacon.js +9 -9
- package/lib/metrics/metrics/beacon.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +10 -0
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +16 -0
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/libp2p/index.d.ts.map +1 -1
- package/lib/network/libp2p/index.js +10 -2
- package/lib/network/libp2p/index.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +7 -2
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/network/reqresp/score.d.ts.map +1 -1
- package/lib/network/reqresp/score.js +5 -11
- package/lib/network/reqresp/score.js.map +1 -1
- package/lib/sync/types.d.ts.map +1 -1
- package/lib/sync/types.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts +9 -0
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +93 -9
- package/lib/sync/unknownBlock.js.map +1 -1
- package/package.json +14 -14
- package/src/api/impl/beacon/blocks/index.ts +4 -6
- package/src/api/impl/beacon/pool/index.ts +5 -0
- package/src/api/impl/events/index.ts +10 -4
- package/src/api/impl/validator/index.ts +1 -1
- package/src/chain/archiveStore/historicalState/metrics.ts +22 -0
- package/src/chain/blocks/importBlock.ts +1 -6
- package/src/chain/builderCircuitBreaker.ts +31 -17
- package/src/chain/errors/proposerPreferences.ts +7 -0
- package/src/chain/options.ts +1 -1
- package/src/chain/prepareNextSlot.ts +1 -1
- package/src/chain/validation/proposerPreferences.ts +12 -2
- package/src/execution/builder/http.ts +11 -11
- package/src/metrics/metrics/beacon.ts +9 -9
- package/src/metrics/metrics/lodestar.ts +16 -0
- package/src/network/libp2p/index.ts +11 -2
- package/src/network/processor/gossipHandlers.ts +9 -0
- package/src/network/reqresp/score.ts +5 -11
- package/src/sync/types.ts +3 -1
- package/src/sync/unknownBlock.ts +108 -12
|
@@ -4,7 +4,8 @@ import {ApiModules} from "../types.js";
|
|
|
4
4
|
|
|
5
5
|
export function getEventsApi({
|
|
6
6
|
chain,
|
|
7
|
-
|
|
7
|
+
logger,
|
|
8
|
+
}: Pick<ApiModules, "chain" | "config" | "logger">): ApplicationMethods<routes.events.Endpoints> {
|
|
8
9
|
return {
|
|
9
10
|
async eventstream({topics, signal, onEvent}) {
|
|
10
11
|
const onAbortFns: (() => void)[] = [];
|
|
@@ -12,9 +13,14 @@ export function getEventsApi({
|
|
|
12
13
|
for (const topic of topics) {
|
|
13
14
|
// biome-ignore lint/suspicious/noExplicitAny: We need to use `any` type here
|
|
14
15
|
const handler = (data: any): void => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
try {
|
|
17
|
+
onEvent({type: topic, message: data});
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// `chain.emitter` emits synchronously, a throwing listener would propagate the error to
|
|
20
|
+
// whoever emitted the event, e.g. a gossip handler, and skip the remaining listeners.
|
|
21
|
+
// A single misbehaving event or client must not affect the node or other subscribers.
|
|
22
|
+
logger.warn("Error sending event to client", {topic}, e as Error);
|
|
23
|
+
}
|
|
18
24
|
};
|
|
19
25
|
|
|
20
26
|
chain.emitter.on(topic, handler);
|
|
@@ -895,7 +895,7 @@ export function getValidatorApi(
|
|
|
895
895
|
// TODO GLOAS: add external builder api support when it is implemented
|
|
896
896
|
const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
|
|
897
897
|
const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
|
|
898
|
-
const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot);
|
|
898
|
+
const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot, parentBlock);
|
|
899
899
|
// Keep a builder bid as fallback unless the circuit breaker is active
|
|
900
900
|
const builderBid = circuitBreakerActive
|
|
901
901
|
? null
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {ForkName} from "@lodestar/params";
|
|
1
2
|
import {
|
|
2
3
|
BeaconStateTransitionMetrics,
|
|
3
4
|
EpochTransitionStep,
|
|
@@ -45,6 +46,27 @@ export function createHistoricalStateTransitionMetrics(
|
|
|
45
46
|
labelNames: ["step"],
|
|
46
47
|
buckets: [0.01, 0.05, 0.1, 0.2, 0.5, 0.75, 1],
|
|
47
48
|
}),
|
|
49
|
+
forkUpgradeTime: metricsRegister.histogram<{fork: ForkName}>({
|
|
50
|
+
name: "lodestar_historical_state_stfn_fork_upgrade_seconds",
|
|
51
|
+
help: "Time to upgrade the state at a fork boundary in seconds",
|
|
52
|
+
labelNames: ["fork"],
|
|
53
|
+
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30],
|
|
54
|
+
}),
|
|
55
|
+
onboardBuildersTime: metricsRegister.histogram({
|
|
56
|
+
name: "lodestar_historical_state_stfn_gloas_onboard_builders_seconds",
|
|
57
|
+
help: "Time spent in onboardBuildersFromPendingDeposits at the gloas fork transition",
|
|
58
|
+
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30],
|
|
59
|
+
}),
|
|
60
|
+
onboardBuildersDeposits: metricsRegister.gauge<{outcome: "onboarded" | "topup" | "kept" | "dropped"}>({
|
|
61
|
+
name: "lodestar_historical_state_stfn_gloas_onboard_builders_deposits",
|
|
62
|
+
help: "Pending deposits handled at the gloas fork transition, by outcome",
|
|
63
|
+
labelNames: ["outcome"],
|
|
64
|
+
}),
|
|
65
|
+
onboardBuildersSignatureChecks: metricsRegister.gauge<{source: "cache" | "verified"}>({
|
|
66
|
+
name: "lodestar_historical_state_stfn_gloas_onboard_builders_signature_checks",
|
|
67
|
+
help: "Builder deposit signature checks at the gloas fork transition, by whether the pre-verify cache served them",
|
|
68
|
+
labelNames: ["source"],
|
|
69
|
+
}),
|
|
48
70
|
processBlockTime: metricsRegister.histogram({
|
|
49
71
|
name: "lodestar_historical_state_stfn_process_block_seconds",
|
|
50
72
|
help: "Time to process a single block in seconds",
|
|
@@ -22,7 +22,6 @@ import {
|
|
|
22
22
|
RootCache,
|
|
23
23
|
computeEpochAtSlot,
|
|
24
24
|
computeTimeAtSlot,
|
|
25
|
-
isStartSlotOfEpoch,
|
|
26
25
|
isStatePostAltair,
|
|
27
26
|
isStatePostBellatrix,
|
|
28
27
|
} from "@lodestar/state-transition";
|
|
@@ -404,11 +403,7 @@ export async function importBlock(
|
|
|
404
403
|
notOverrideFcuReason = NotReorgedReason.NotProposerOfNextSlot;
|
|
405
404
|
}
|
|
406
405
|
} catch (e) {
|
|
407
|
-
|
|
408
|
-
notOverrideFcuReason = NotReorgedReason.AtEpochBoundary;
|
|
409
|
-
} else {
|
|
410
|
-
this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error);
|
|
411
|
-
}
|
|
406
|
+
this.logger.warn("Unable to get beacon proposer. Do not override fcu.", {proposalSlot}, e as Error);
|
|
412
407
|
}
|
|
413
408
|
|
|
414
409
|
if (shouldOverrideFcu) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {IForkChoice} from "@lodestar/fork-choice";
|
|
1
|
+
import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
|
|
2
2
|
import {Slot} from "@lodestar/types";
|
|
3
3
|
import {Logger} from "@lodestar/utils";
|
|
4
4
|
import {getFaultInspectionParams} from "../execution/builder/http.js";
|
|
@@ -15,11 +15,15 @@ export type BuilderCircuitBreakerModules = {
|
|
|
15
15
|
metrics: Metrics | null;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
/** Four observations is the minimum useful recovery sample for the default ~25% fault budget */
|
|
19
|
+
const MIN_BLOCKS_TO_DEACTIVATE = 4;
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* Post-gloas circuit breaker for builder bids. The beacon block is produced by the proposer
|
|
20
23
|
* regardless of bid source, so missed blocks are not a useful builder health signal. Instead
|
|
21
|
-
* count blocks
|
|
22
|
-
*
|
|
24
|
+
* count canonical blocks selected as EMPTY. Activate when the proportion of EMPTY blocks exceeds
|
|
25
|
+
* the fault budget, and resume selecting builder bids only when the observed blocks are within
|
|
26
|
+
* budget and meet the minimum recovery sample size.
|
|
23
27
|
*/
|
|
24
28
|
export class BuilderCircuitBreaker {
|
|
25
29
|
readonly faultInspectionWindow: number;
|
|
@@ -35,39 +39,49 @@ export class BuilderCircuitBreaker {
|
|
|
35
39
|
const {faultInspectionWindow, allowedFaults} = getFaultInspectionParams(opts);
|
|
36
40
|
this.faultInspectionWindow = faultInspectionWindow;
|
|
37
41
|
this.allowedFaults = allowedFaults;
|
|
42
|
+
this.modules.logger.info("Builder circuit breaker initialized", {faultInspectionWindow, allowedFaults});
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
/** Whether builder bids must be ignored for a block produced at clockSlot */
|
|
41
|
-
isActive(clockSlot: Slot): boolean {
|
|
42
|
-
this.update(clockSlot);
|
|
46
|
+
isActive(clockSlot: Slot, head: ProtoBlock): boolean {
|
|
47
|
+
this.update(clockSlot, head);
|
|
43
48
|
return this.active;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
update(clockSlot: Slot): void {
|
|
51
|
+
update(clockSlot: Slot, head: ProtoBlock): void {
|
|
47
52
|
if (clockSlot <= this.lastUpdatedSlot) {
|
|
48
53
|
return;
|
|
49
54
|
}
|
|
50
55
|
this.lastUpdatedSlot = clockSlot;
|
|
51
56
|
|
|
52
|
-
// Exclude clockSlot itself, its payload
|
|
53
|
-
const {
|
|
57
|
+
// Exclude clockSlot itself, its payload status may still be unresolved
|
|
58
|
+
const {full, empty} = this.modules.forkChoice.getCanonicalPayloadCounts(
|
|
54
59
|
Math.max(clockSlot - this.faultInspectionWindow, 0),
|
|
55
|
-
clockSlot - 1
|
|
60
|
+
clockSlot - 1,
|
|
61
|
+
head
|
|
56
62
|
);
|
|
57
|
-
const
|
|
63
|
+
const canonicalBlocks = full + empty;
|
|
58
64
|
|
|
59
65
|
const wasActive = this.active;
|
|
60
|
-
// Scale the fault budget by blocks
|
|
61
|
-
|
|
66
|
+
// Scale the fault budget by canonical blocks so sparse windows still trigger on high EMPTY rates
|
|
67
|
+
const exceedsFaultBudget = empty * this.faultInspectionWindow > this.allowedFaults * canonicalBlocks;
|
|
68
|
+
if (exceedsFaultBudget) {
|
|
69
|
+
this.active = true;
|
|
70
|
+
} else if (canonicalBlocks >= MIN_BLOCKS_TO_DEACTIVATE) {
|
|
71
|
+
// Require a minimum sample within the fault budget before accepting builder bids again
|
|
72
|
+
this.active = false;
|
|
73
|
+
}
|
|
62
74
|
|
|
63
75
|
this.modules.metrics?.builderCircuitBreaker.active.set(this.active ? 1 : 0);
|
|
64
|
-
this.modules.metrics?.builderCircuitBreaker.
|
|
65
|
-
this.modules.metrics?.builderCircuitBreaker.
|
|
66
|
-
this.modules.metrics?.builderCircuitBreaker.
|
|
76
|
+
this.modules.metrics?.builderCircuitBreaker.canonicalBlocks.set(canonicalBlocks);
|
|
77
|
+
this.modules.metrics?.builderCircuitBreaker.fullBlocks.set(full);
|
|
78
|
+
this.modules.metrics?.builderCircuitBreaker.emptyBlocks.set(empty);
|
|
67
79
|
|
|
68
80
|
const logCtx = {
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
clockSlot,
|
|
82
|
+
canonicalBlocks,
|
|
83
|
+
full,
|
|
84
|
+
empty,
|
|
71
85
|
faultInspectionWindow: this.faultInspectionWindow,
|
|
72
86
|
allowedFaults: this.allowedFaults,
|
|
73
87
|
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import {ForkName} from "@lodestar/params";
|
|
1
2
|
import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
|
|
2
3
|
import {GossipActionError} from "./gossipValidation.js";
|
|
3
4
|
|
|
4
5
|
export enum ProposerPreferencesErrorCode {
|
|
6
|
+
PRE_GLOAS_PROPOSAL_SLOT = "PROPOSER_PREFERENCES_ERROR_PRE_GLOAS_PROPOSAL_SLOT",
|
|
5
7
|
INVALID_EPOCH = "PROPOSER_PREFERENCES_ERROR_INVALID_EPOCH",
|
|
6
8
|
PROPOSAL_SLOT_PASSED = "PROPOSER_PREFERENCES_ERROR_PROPOSAL_SLOT_PASSED",
|
|
7
9
|
UNKNOWN_DEPENDENT_ROOT = "PROPOSER_PREFERENCES_ERROR_UNKNOWN_DEPENDENT_ROOT",
|
|
@@ -13,6 +15,11 @@ export enum ProposerPreferencesErrorCode {
|
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export type ProposerPreferencesErrorType =
|
|
18
|
+
| {
|
|
19
|
+
code: ProposerPreferencesErrorCode.PRE_GLOAS_PROPOSAL_SLOT;
|
|
20
|
+
proposalSlot: Slot;
|
|
21
|
+
proposalFork: ForkName;
|
|
22
|
+
}
|
|
16
23
|
| {
|
|
17
24
|
code: ProposerPreferencesErrorCode.INVALID_EPOCH;
|
|
18
25
|
proposalSlot: Slot;
|
package/src/chain/options.ts
CHANGED
|
@@ -53,7 +53,7 @@ export type IChainOptions = BlockProcessOpts &
|
|
|
53
53
|
nativeStateView?: boolean;
|
|
54
54
|
/** Builder circuit breaker fault inspection window in slots */
|
|
55
55
|
faultInspectionWindow?: number;
|
|
56
|
-
/**
|
|
56
|
+
/** Canonical EMPTY blocks allowed per `faultInspectionWindow` observed blocks */
|
|
57
57
|
allowedFaults?: number;
|
|
58
58
|
};
|
|
59
59
|
|
|
@@ -169,7 +169,7 @@ export class PrepareNextSlotScheduler {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
if (isForkPostGloas(fork)) {
|
|
172
|
-
this.chain.builderCircuitBreaker.update(clockSlot);
|
|
172
|
+
this.chain.builderCircuitBreaker.update(clockSlot, updatedHead);
|
|
173
173
|
} else {
|
|
174
174
|
// Update the builder status, if enabled shoot an api call to check status
|
|
175
175
|
this.chain.updateBuilderStatus(clockSlot);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
|
|
2
|
-
import {GENESIS_EPOCH, GENESIS_SLOT, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
|
|
2
|
+
import {GENESIS_EPOCH, GENESIS_SLOT, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
|
|
3
3
|
import {
|
|
4
4
|
computeEpochAtSlot,
|
|
5
5
|
computeStartSlotAtEpoch,
|
|
@@ -13,7 +13,7 @@ import {IBeaconChain} from "../index.js";
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Validates a gossiped `SignedProposerPreferences` per
|
|
16
|
-
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.
|
|
16
|
+
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.14/specs/gloas/p2p-interface.md#new-proposer_preferences
|
|
17
17
|
*/
|
|
18
18
|
export async function validateGossipProposerPreferences(
|
|
19
19
|
chain: IBeaconChain,
|
|
@@ -24,6 +24,16 @@ export async function validateGossipProposerPreferences(
|
|
|
24
24
|
const dependentRootHex = toRootHex(dependentRoot);
|
|
25
25
|
const proposalEpoch = computeEpochAtSlot(proposalSlot);
|
|
26
26
|
|
|
27
|
+
// [IGNORE] The proposal epoch is after the Gloas upgrade
|
|
28
|
+
const proposalFork = chain.config.getForkName(proposalSlot);
|
|
29
|
+
if (!isForkPostGloas(proposalFork)) {
|
|
30
|
+
throw new ProposerPreferencesError(GossipAction.IGNORE, {
|
|
31
|
+
code: ProposerPreferencesErrorCode.PRE_GLOAS_PROPOSAL_SLOT,
|
|
32
|
+
proposalSlot,
|
|
33
|
+
proposalFork,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
// [IGNORE] `preferences.proposal_slot` is within the proposer lookahead,
|
|
28
38
|
// allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
|
|
29
39
|
const currentEpoch = computeEpochAtSlot(chain.clock.currentSlotWithGossipDisparity);
|
|
@@ -71,15 +71,18 @@ export class NoBidReceived extends Error {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
75
|
-
* the circuit breaker (so at boot time and once for each unique boot).
|
|
74
|
+
* Default circuit breaker parameters:
|
|
76
75
|
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
76
|
+
* SLOTS_PER_EPOCH <= FAULT_INSPECTION_WINDOW < 2 * SLOTS_PER_EPOCH (randomized at initialization)
|
|
77
|
+
* ALLOWED_FAULTS: FAULT_INSPECTION_WINDOW // 4
|
|
79
78
|
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
79
|
+
* e.g. on mainnet SLOTS_PER_EPOCH is 32, so FAULT_INSPECTION_WINDOW is between 32 and 63,
|
|
80
|
+
* and a window of 40 results in ALLOWED_FAULTS = 10.
|
|
81
|
+
*
|
|
82
|
+
* The randomized default window keeps the exact slots under inspection unpredictable per node,
|
|
83
|
+
* so a builder cannot tell when past faults age out of a given proposer's window and time
|
|
84
|
+
* withholding around trip or recovery points. Explicitly configured windows are clamped to at
|
|
85
|
+
* least SLOTS_PER_EPOCH, and configured allowedFaults is capped at the default ~25% budget.
|
|
83
86
|
*/
|
|
84
87
|
export function getFaultInspectionParams(opts: {faultInspectionWindow?: number; allowedFaults?: number}): {
|
|
85
88
|
faultInspectionWindow: number;
|
|
@@ -90,10 +93,7 @@ export function getFaultInspectionParams(opts: {faultInspectionWindow?: number;
|
|
|
90
93
|
SLOTS_PER_EPOCH
|
|
91
94
|
);
|
|
92
95
|
// allowedFaults should be < faultInspectionWindow, limiting them to faultInspectionWindow/4
|
|
93
|
-
const allowedFaults = Math.min(
|
|
94
|
-
opts.allowedFaults ?? Math.floor(faultInspectionWindow / 4),
|
|
95
|
-
Math.floor(faultInspectionWindow / 4)
|
|
96
|
-
);
|
|
96
|
+
const allowedFaults = Math.min(opts.allowedFaults ?? Infinity, Math.floor(faultInspectionWindow / 4));
|
|
97
97
|
return {faultInspectionWindow, allowedFaults};
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -143,17 +143,17 @@ export function createBeaconMetrics(register: RegistryMetricCreator) {
|
|
|
143
143
|
name: "beacon_builder_circuit_breaker_active",
|
|
144
144
|
help: "Whether the builder circuit breaker is active (1) causing builder bids to be ignored",
|
|
145
145
|
}),
|
|
146
|
-
|
|
147
|
-
name: "
|
|
148
|
-
help: "Count of
|
|
146
|
+
canonicalBlocks: register.gauge({
|
|
147
|
+
name: "beacon_builder_circuit_breaker_canonical_blocks",
|
|
148
|
+
help: "Count of canonical FULL and EMPTY blocks in the fault inspection window",
|
|
149
149
|
}),
|
|
150
|
-
|
|
151
|
-
name: "
|
|
152
|
-
help: "Count of blocks
|
|
150
|
+
fullBlocks: register.gauge({
|
|
151
|
+
name: "beacon_builder_circuit_breaker_full_blocks",
|
|
152
|
+
help: "Count of canonical FULL blocks in the fault inspection window",
|
|
153
153
|
}),
|
|
154
|
-
|
|
155
|
-
name: "
|
|
156
|
-
help: "Count of
|
|
154
|
+
emptyBlocks: register.gauge({
|
|
155
|
+
name: "beacon_builder_circuit_breaker_empty_blocks",
|
|
156
|
+
help: "Count of canonical EMPTY blocks in the fault inspection window",
|
|
157
157
|
}),
|
|
158
158
|
},
|
|
159
159
|
|
|
@@ -908,6 +908,22 @@ export function createLodestarMetrics(
|
|
|
908
908
|
labelNames: ["error"],
|
|
909
909
|
}),
|
|
910
910
|
},
|
|
911
|
+
gossipExecutionPayloadBid: {
|
|
912
|
+
elapsedTimeTillReceived: register.histogram<{source: OpSource}>({
|
|
913
|
+
name: "lodestar_gossip_execution_payload_bid_elapsed_time_till_received_seconds",
|
|
914
|
+
help: "Time elapsed between the bid's slot start and the time the execution payload bid was received (negative = received before its slot, broadcast in the previous slot; positive = received during its own slot, late)",
|
|
915
|
+
labelNames: ["source"],
|
|
916
|
+
buckets: [-6, -3, -1, 0, 3],
|
|
917
|
+
}),
|
|
918
|
+
},
|
|
919
|
+
gossipPayloadAttestationMessage: {
|
|
920
|
+
elapsedTimeTillReceived: register.histogram<{source: OpSource}>({
|
|
921
|
+
name: "lodestar_gossip_payload_attestation_message_elapsed_time_till_received_seconds",
|
|
922
|
+
help: "Time elapsed between slot time and the time the payload attestation message was received",
|
|
923
|
+
labelNames: ["source"],
|
|
924
|
+
buckets: [3, 6, 9, 12],
|
|
925
|
+
}),
|
|
926
|
+
},
|
|
911
927
|
// recovery in the case of specific blob rows required
|
|
912
928
|
recoverBlobSidecars: {
|
|
913
929
|
blobsReconstructed: register.counter({
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {getCiphers} from "node:crypto";
|
|
1
2
|
import {bootstrap} from "@libp2p/bootstrap";
|
|
2
3
|
import {identify} from "@libp2p/identify";
|
|
3
4
|
import type {PrivateKey} from "@libp2p/interface";
|
|
@@ -9,12 +10,20 @@ import {Libp2pInit, createLibp2p} from "libp2p";
|
|
|
9
10
|
import {Registry} from "prom-client";
|
|
10
11
|
import {ENR} from "@chainsafe/enr";
|
|
11
12
|
import {noise} from "@chainsafe/libp2p-noise";
|
|
12
|
-
import {defaultCrypto} from "@chainsafe/libp2p-noise/crypto";
|
|
13
|
+
import {asCrypto, defaultCrypto} from "@chainsafe/libp2p-noise/crypto";
|
|
13
14
|
import {quic} from "@chainsafe/libp2p-quic";
|
|
14
15
|
import {Libp2p, LodestarComponents} from "../interface.js";
|
|
15
16
|
import {NetworkOptions, defaultNetworkOptions} from "../options.js";
|
|
16
17
|
import {Eth2PeerDataStore} from "../peers/datastore.js";
|
|
17
18
|
|
|
19
|
+
const noiseCrypto = getCiphers().includes("chacha20-poly1305")
|
|
20
|
+
? defaultCrypto
|
|
21
|
+
: {
|
|
22
|
+
...defaultCrypto,
|
|
23
|
+
chaCha20Poly1305Encrypt: asCrypto.chaCha20Poly1305Encrypt,
|
|
24
|
+
chaCha20Poly1305Decrypt: asCrypto.chaCha20Poly1305Decrypt,
|
|
25
|
+
};
|
|
26
|
+
|
|
18
27
|
export type NodeJsLibp2pOpts = {
|
|
19
28
|
peerStoreDir?: string;
|
|
20
29
|
disablePeerDiscovery?: boolean;
|
|
@@ -122,7 +131,7 @@ export async function createNodeJsLibp2p(
|
|
|
122
131
|
listen: localMultiaddrs,
|
|
123
132
|
announce: [],
|
|
124
133
|
},
|
|
125
|
-
connectionEncrypters: [noise({crypto:
|
|
134
|
+
connectionEncrypters: [noise({crypto: noiseCrypto})],
|
|
126
135
|
transports,
|
|
127
136
|
streamMuxers: [mplex({disconnectThreshold})],
|
|
128
137
|
peerDiscovery,
|
|
@@ -1255,11 +1255,15 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1255
1255
|
[GossipType.payload_attestation_message]: async ({
|
|
1256
1256
|
gossipData,
|
|
1257
1257
|
topic,
|
|
1258
|
+
seenTimestampSec,
|
|
1258
1259
|
}: GossipHandlerParamGeneric<GossipType.payload_attestation_message>) => {
|
|
1259
1260
|
const {serializedData} = gossipData;
|
|
1260
1261
|
const payloadAttestationMessage = sszDeserialize(topic, serializedData);
|
|
1261
1262
|
const validationResult = await validateGossipPayloadAttestationMessage(chain, payloadAttestationMessage);
|
|
1262
1263
|
|
|
1264
|
+
const delaySec = chain.clock.secFromSlot(payloadAttestationMessage.data.slot, seenTimestampSec);
|
|
1265
|
+
metrics?.gossipPayloadAttestationMessage.elapsedTimeTillReceived.observe({source: OpSource.gossip}, delaySec);
|
|
1266
|
+
|
|
1263
1267
|
try {
|
|
1264
1268
|
const insertOutcome = chain.payloadAttestationPool.add(
|
|
1265
1269
|
payloadAttestationMessage,
|
|
@@ -1286,11 +1290,16 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1286
1290
|
[GossipType.execution_payload_bid]: async ({
|
|
1287
1291
|
gossipData,
|
|
1288
1292
|
topic,
|
|
1293
|
+
seenTimestampSec,
|
|
1289
1294
|
}: GossipHandlerParamGeneric<GossipType.execution_payload_bid>) => {
|
|
1290
1295
|
const {serializedData} = gossipData;
|
|
1291
1296
|
const executionPayloadBid = sszDeserialize(topic, serializedData);
|
|
1292
1297
|
const {proposerIndex} = await validateGossipExecutionPayloadBid(chain, executionPayloadBid);
|
|
1293
1298
|
|
|
1299
|
+
// this could be negative, because it's most likely the bid of next slot comes at this clock slot
|
|
1300
|
+
const elapsedSec = chain.clock.secFromSlot(executionPayloadBid.message.slot, seenTimestampSec);
|
|
1301
|
+
metrics?.gossipExecutionPayloadBid.elapsedTimeTillReceived.observe({source: OpSource.gossip}, elapsedSec);
|
|
1302
|
+
|
|
1294
1303
|
// Handle valid payload bid by storing in a bid pool
|
|
1295
1304
|
try {
|
|
1296
1305
|
const insertOutcome = chain.executionPayloadBidPool.add(executionPayloadBid);
|
|
@@ -44,17 +44,11 @@ export function onOutgoingReqRespError(e: RequestError, method: ReqRespMethod):
|
|
|
44
44
|
// failure, so keep the stronger penalty (Fatal for Ping, as before).
|
|
45
45
|
return method === ReqRespMethod.Ping ? PeerAction.Fatal : PeerAction.LowToleranceError;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
case ReqRespMethod.Ping:
|
|
53
|
-
case ReqRespMethod.Status:
|
|
54
|
-
return PeerAction.HighToleranceError;
|
|
55
|
-
default:
|
|
56
|
-
return PeerAction.LowToleranceError;
|
|
57
|
-
}
|
|
47
|
+
// A dial failure means we could not open a stream. That is evidence about the transport, not
|
|
48
|
+
// about the peer's behavior, and it is dominated by transient congestion on our side, so
|
|
49
|
+
// penalize leniently for every method to avoid self-inflicted peer starvation (#9562). A
|
|
50
|
+
// genuinely dead peer still accumulates penalty and is eventually evicted, freeing the slot.
|
|
51
|
+
return PeerAction.HighToleranceError;
|
|
58
52
|
// TODO: Detect SSZDecodeError and return PeerAction.Fatal
|
|
59
53
|
|
|
60
54
|
case RequestErrorCode.RESP_TIMEOUT:
|
package/src/sync/types.ts
CHANGED
|
@@ -77,7 +77,9 @@ export type PendingPayloadInput = {
|
|
|
77
77
|
export type PendingPayloadRootHex = {
|
|
78
78
|
status: PendingPayloadInputStatus.pending | PendingPayloadInputStatus.fetching;
|
|
79
79
|
rootHex: RootHex;
|
|
80
|
-
//
|
|
80
|
+
// Trusted slot only (fork choice / validated data), may be missing until resolved. NOT the gossip
|
|
81
|
+
// message slot from ChainEvent.unknownEnvelopeBlockRoot, which is untrusted and not necessarily the
|
|
82
|
+
// payload/block slot. See BlockInputSync.resolvePayloadSlot.
|
|
81
83
|
slot?: Slot;
|
|
82
84
|
timeAddedSec: number;
|
|
83
85
|
timeSyncedSec?: number;
|