@lodestar/beacon-node 1.47.0-dev.404f68958c → 1.47.0-dev.6325e4e1fe
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 +0 -6
- package/lib/api/impl/beacon/blocks/index.js.map +1 -1
- package/lib/api/impl/lodestar/index.d.ts.map +1 -1
- package/lib/api/impl/lodestar/index.js +11 -12
- package/lib/api/impl/lodestar/index.js.map +1 -1
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +12 -22
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/api/impl/validator/utils.d.ts +5 -0
- package/lib/api/impl/validator/utils.d.ts.map +1 -1
- package/lib/api/impl/validator/utils.js +18 -12
- package/lib/api/impl/validator/utils.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/index.js +1 -1
- package/lib/chain/blocks/index.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/chain.d.ts +1 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +2 -1
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/errors/blockError.d.ts +1 -0
- package/lib/chain/errors/blockError.d.ts.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/produceBlock/produceBlockBody.d.ts +2 -0
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +6 -1
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/seenCache/seenBlockProposers.d.ts +5 -1
- package/lib/chain/seenCache/seenBlockProposers.d.ts.map +1 -1
- package/lib/chain/seenCache/seenBlockProposers.js +10 -3
- package/lib/chain/seenCache/seenBlockProposers.js.map +1 -1
- package/lib/chain/validation/block.d.ts.map +1 -1
- package/lib/chain/validation/block.js +21 -7
- package/lib/chain/validation/block.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/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +25 -3
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +17 -5
- package/lib/sync/unknownBlock.js.map +1 -1
- package/package.json +14 -14
- package/src/api/impl/beacon/blocks/index.ts +0 -6
- package/src/api/impl/lodestar/index.ts +11 -12
- package/src/api/impl/validator/index.ts +22 -27
- package/src/api/impl/validator/utils.ts +30 -15
- package/src/chain/archiveStore/historicalState/metrics.ts +22 -0
- package/src/chain/blocks/index.ts +1 -1
- package/src/chain/builderCircuitBreaker.ts +31 -17
- package/src/chain/chain.ts +2 -0
- package/src/chain/errors/blockError.ts +1 -1
- package/src/chain/options.ts +1 -1
- package/src/chain/prepareNextSlot.ts +1 -1
- package/src/chain/produceBlock/produceBlockBody.ts +13 -0
- package/src/chain/seenCache/seenBlockProposers.ts +13 -3
- package/src/chain/validation/block.ts +21 -7
- package/src/execution/builder/http.ts +11 -11
- package/src/metrics/metrics/beacon.ts +9 -9
- package/src/network/processor/gossipHandlers.ts +26 -3
- package/src/sync/unknownBlock.ts +18 -5
|
@@ -92,7 +92,12 @@ import {getStateResponseWithRegen} from "../beacon/state/utils.js";
|
|
|
92
92
|
import {ApiError, FailureList, IndexedError, NodeIsSyncing, OnlySupportedByDVT} from "../errors.js";
|
|
93
93
|
import {ApiModules} from "../types.js";
|
|
94
94
|
import {notWhileSyncing} from "../utils.js";
|
|
95
|
-
import {
|
|
95
|
+
import {
|
|
96
|
+
computeSubnetForCommitteesAtSlot,
|
|
97
|
+
getPubkeysForIndices,
|
|
98
|
+
selectBlockProductionSource,
|
|
99
|
+
selectBlockProductionSourceByBoostFactor,
|
|
100
|
+
} from "./utils.js";
|
|
96
101
|
|
|
97
102
|
/**
|
|
98
103
|
* Cutoff time to wait from start of the slot for execution and builder block production apis to resolve.
|
|
@@ -577,8 +582,7 @@ export function getValidatorApi(
|
|
|
577
582
|
builderSelection,
|
|
578
583
|
isBuilderEnabled,
|
|
579
584
|
strictFeeRecipientCheck,
|
|
580
|
-
|
|
581
|
-
builderBoostFactor: `${builderBoostFactor}`,
|
|
585
|
+
builderBoostFactor,
|
|
582
586
|
};
|
|
583
587
|
|
|
584
588
|
logger.verbose("Assembling block with produceEngineOrBuilderBlock", loggerContext);
|
|
@@ -851,8 +855,8 @@ export function getValidatorApi(
|
|
|
851
855
|
randaoReveal,
|
|
852
856
|
graffiti,
|
|
853
857
|
feeRecipient,
|
|
858
|
+
strictFeeRecipientCheck,
|
|
854
859
|
includePayload,
|
|
855
|
-
builderSelection,
|
|
856
860
|
builderBoostFactor,
|
|
857
861
|
}) {
|
|
858
862
|
const fork = config.getForkName(slot);
|
|
@@ -861,11 +865,6 @@ export function getValidatorApi(
|
|
|
861
865
|
throw new ApiError(400, `produceBlockV4 not supported for pre-gloas fork=${fork}`);
|
|
862
866
|
}
|
|
863
867
|
|
|
864
|
-
builderSelection = builderSelection ?? routes.validator.BuilderSelection.MaxProfit;
|
|
865
|
-
if (builderSelection === routes.validator.BuilderSelection.BuilderOnly) {
|
|
866
|
-
logger.warn("Builder selection builderonly is no longer supported, treating as builderalways");
|
|
867
|
-
builderSelection = routes.validator.BuilderSelection.BuilderAlways;
|
|
868
|
-
}
|
|
869
868
|
builderBoostFactor = builderBoostFactor ?? BigInt(100);
|
|
870
869
|
if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
|
|
871
870
|
throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
|
|
@@ -896,14 +895,11 @@ export function getValidatorApi(
|
|
|
896
895
|
// TODO GLOAS: add external builder api support when it is implemented
|
|
897
896
|
const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
|
|
898
897
|
const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
|
|
899
|
-
|
|
900
|
-
//
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
builderSelection === routes.validator.BuilderSelection.ExecutionOnly || circuitBreakerActive
|
|
905
|
-
? null
|
|
906
|
-
: chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
|
|
898
|
+
const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot, parentBlock);
|
|
899
|
+
// Keep a builder bid as fallback unless the circuit breaker is active
|
|
900
|
+
const builderBid = circuitBreakerActive
|
|
901
|
+
? null
|
|
902
|
+
: chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
|
|
907
903
|
|
|
908
904
|
const logCtx = {
|
|
909
905
|
slot,
|
|
@@ -911,8 +907,8 @@ export function getValidatorApi(
|
|
|
911
907
|
parentBlockRoot: parentBlockRootHex,
|
|
912
908
|
parentBlockHash: parentBlock.executionPayloadBlockHash,
|
|
913
909
|
fork,
|
|
914
|
-
builderSelection,
|
|
915
910
|
builderBoostFactor,
|
|
911
|
+
strictFeeRecipientCheck,
|
|
916
912
|
circuitBreakerActive,
|
|
917
913
|
...(builderBid !== null
|
|
918
914
|
? {
|
|
@@ -936,6 +932,7 @@ export function getValidatorApi(
|
|
|
936
932
|
randaoReveal,
|
|
937
933
|
graffiti: graffitiBytes,
|
|
938
934
|
feeRecipient,
|
|
935
|
+
strictFeeRecipientCheck,
|
|
939
936
|
commonBlockBodyPromise,
|
|
940
937
|
};
|
|
941
938
|
|
|
@@ -960,12 +957,8 @@ export function getValidatorApi(
|
|
|
960
957
|
chain.produceBlock(baseAttrs)
|
|
961
958
|
).then((engineBlock) => {
|
|
962
959
|
// No need to wait for the bid block if the engine block will always be selected due to
|
|
963
|
-
// suspected builder censorship
|
|
964
|
-
if (
|
|
965
|
-
engineBlock.shouldOverrideBuilder ||
|
|
966
|
-
builderBoostFactor === BigInt(0) ||
|
|
967
|
-
builderSelection === routes.validator.BuilderSelection.ExecutionAlways
|
|
968
|
-
) {
|
|
960
|
+
// suspected builder censorship or a builder boost factor of 0
|
|
961
|
+
if (engineBlock.shouldOverrideBuilder || builderBoostFactor === BigInt(0)) {
|
|
969
962
|
controller.abort();
|
|
970
963
|
}
|
|
971
964
|
return engineBlock;
|
|
@@ -994,8 +987,7 @@ export function getValidatorApi(
|
|
|
994
987
|
});
|
|
995
988
|
logger.warn("Selected local block: censorship suspected in builder bid", logCtx);
|
|
996
989
|
} else if (engineResult.status === "fulfilled" && bidResult.status === "fulfilled") {
|
|
997
|
-
const result =
|
|
998
|
-
builderSelection,
|
|
990
|
+
const result = selectBlockProductionSourceByBoostFactor({
|
|
999
991
|
builderBoostFactor,
|
|
1000
992
|
engineExecutionPayloadValue: engineResult.value.executionPayloadValue,
|
|
1001
993
|
// The bid value is the payment to the proposer, in Gwei
|
|
@@ -1061,7 +1053,10 @@ export function getValidatorApi(
|
|
|
1061
1053
|
root: blockRoot,
|
|
1062
1054
|
});
|
|
1063
1055
|
if (chain.opts.persistProducedBlocks) {
|
|
1064
|
-
void chain.persistBlock(
|
|
1056
|
+
void chain.persistBlock(
|
|
1057
|
+
block,
|
|
1058
|
+
source === ProducedBlockSource.builder ? "produced_builder_block" : "produced_engine_block"
|
|
1059
|
+
);
|
|
1065
1060
|
}
|
|
1066
1061
|
|
|
1067
1062
|
// Include the payload for self-builds unless disabled (stateless flow)
|
|
@@ -59,24 +59,39 @@ export function selectBlockProductionSource({
|
|
|
59
59
|
return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.EnginePreferred};
|
|
60
60
|
|
|
61
61
|
case routes.validator.BuilderSelection.Default:
|
|
62
|
-
case routes.validator.BuilderSelection.MaxProfit:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (engineExecutionPayloadValue >= (builderExecutionPayloadValue * builderBoostFactor) / BigInt(100)) {
|
|
72
|
-
return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.BlockValue};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BlockValue};
|
|
76
|
-
}
|
|
62
|
+
case routes.validator.BuilderSelection.MaxProfit:
|
|
63
|
+
return selectBlockProductionSourceByBoostFactor({
|
|
64
|
+
engineExecutionPayloadValue,
|
|
65
|
+
builderExecutionPayloadValue,
|
|
66
|
+
builderBoostFactor,
|
|
67
|
+
});
|
|
77
68
|
|
|
78
69
|
case routes.validator.BuilderSelection.BuilderAlways:
|
|
79
70
|
case routes.validator.BuilderSelection.BuilderOnly:
|
|
80
71
|
return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
|
|
81
72
|
}
|
|
82
73
|
}
|
|
74
|
+
|
|
75
|
+
export function selectBlockProductionSourceByBoostFactor({
|
|
76
|
+
engineExecutionPayloadValue,
|
|
77
|
+
builderExecutionPayloadValue,
|
|
78
|
+
builderBoostFactor,
|
|
79
|
+
}: {
|
|
80
|
+
engineExecutionPayloadValue: bigint;
|
|
81
|
+
builderExecutionPayloadValue: bigint;
|
|
82
|
+
builderBoostFactor: bigint;
|
|
83
|
+
}): BlockSelectionResult {
|
|
84
|
+
if (builderBoostFactor === BigInt(0)) {
|
|
85
|
+
return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.EnginePreferred};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (builderBoostFactor === MAX_BUILDER_BOOST_FACTOR) {
|
|
89
|
+
return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (engineExecutionPayloadValue >= (builderExecutionPayloadValue * builderBoostFactor) / BigInt(100)) {
|
|
93
|
+
return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.BlockValue};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BlockValue};
|
|
97
|
+
}
|
|
@@ -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",
|
|
@@ -110,7 +110,7 @@ export async function processBlocks(
|
|
|
110
110
|
|
|
111
111
|
for (const blockInput of relevantBlocks) {
|
|
112
112
|
const block = blockInput.getBlock().message;
|
|
113
|
-
this.seenBlockProposers.add(block.slot, block.proposerIndex);
|
|
113
|
+
this.seenBlockProposers.add(block.slot, block.proposerIndex, blockInput.blockRootHex);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
const {executionStatuses} = segmentExecStatus;
|
|
@@ -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
|
};
|
package/src/chain/chain.ts
CHANGED
|
@@ -1072,6 +1072,7 @@ export class BeaconChain implements IBeaconChain {
|
|
|
1072
1072
|
graffiti,
|
|
1073
1073
|
slot,
|
|
1074
1074
|
feeRecipient,
|
|
1075
|
+
strictFeeRecipientCheck,
|
|
1075
1076
|
commonBlockBodyPromise,
|
|
1076
1077
|
parentBlock,
|
|
1077
1078
|
builderBid,
|
|
@@ -1100,6 +1101,7 @@ export class BeaconChain implements IBeaconChain {
|
|
|
1100
1101
|
graffiti,
|
|
1101
1102
|
slot,
|
|
1102
1103
|
feeRecipient,
|
|
1104
|
+
strictFeeRecipientCheck,
|
|
1103
1105
|
parentBlock,
|
|
1104
1106
|
proposerIndex,
|
|
1105
1107
|
proposerPubKey,
|
|
@@ -118,7 +118,7 @@ export type BlockErrorType =
|
|
|
118
118
|
| {code: BlockErrorCode.GENESIS_BLOCK}
|
|
119
119
|
| {code: BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT; blockSlot: Slot; finalizedSlot: Slot}
|
|
120
120
|
| {code: BlockErrorCode.ALREADY_KNOWN; root: RootHex}
|
|
121
|
-
| {code: BlockErrorCode.REPEAT_PROPOSAL; proposerIndex: ValidatorIndex}
|
|
121
|
+
| {code: BlockErrorCode.REPEAT_PROPOSAL; proposerIndex: ValidatorIndex; root: RootHex}
|
|
122
122
|
| {code: BlockErrorCode.BLOCK_SLOT_LIMIT_REACHED}
|
|
123
123
|
| {code: BlockErrorCode.INCORRECT_PROPOSER; proposerIndex: ValidatorIndex}
|
|
124
124
|
| {code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID; blockSlot: 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);
|
|
@@ -100,6 +100,8 @@ export type BlockAttributes = {
|
|
|
100
100
|
slot: Slot;
|
|
101
101
|
parentBlock: ProtoBlock;
|
|
102
102
|
feeRecipient?: string;
|
|
103
|
+
/** Verify that a locally produced execution payload uses `feeRecipient`. */
|
|
104
|
+
strictFeeRecipientCheck?: boolean;
|
|
103
105
|
/** When provided, build block with this builder bid instead of a self-build bid */
|
|
104
106
|
builderBid?: gloas.SignedExecutionPayloadBid;
|
|
105
107
|
};
|
|
@@ -201,6 +203,7 @@ export async function produceBlockBody<T extends BlockType>(
|
|
|
201
203
|
const {
|
|
202
204
|
slot: blockSlot,
|
|
203
205
|
feeRecipient: requestedFeeRecipient,
|
|
206
|
+
strictFeeRecipientCheck,
|
|
204
207
|
parentBlock,
|
|
205
208
|
proposerIndex,
|
|
206
209
|
proposerPubKey,
|
|
@@ -334,6 +337,16 @@ export async function produceBlockBody<T extends BlockType>(
|
|
|
334
337
|
executionPayloadValue = payloadRes.executionPayloadValue;
|
|
335
338
|
shouldOverrideBuilder = payloadRes.shouldOverrideBuilder;
|
|
336
339
|
|
|
340
|
+
if (
|
|
341
|
+
strictFeeRecipientCheck &&
|
|
342
|
+
requestedFeeRecipient &&
|
|
343
|
+
!byteArrayEquals(executionPayload.feeRecipient, fromHex(requestedFeeRecipient))
|
|
344
|
+
) {
|
|
345
|
+
throw Error(
|
|
346
|
+
`Invalid feeRecipient set in engine payload expected=${requestedFeeRecipient} actual=${toHex(executionPayload.feeRecipient)}`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
337
350
|
if (blobsBundle === undefined) {
|
|
338
351
|
throw Error(`Missing blobsBundle response from getPayload at fork=${fork}`);
|
|
339
352
|
}
|
|
@@ -19,7 +19,9 @@ const MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL = 2;
|
|
|
19
19
|
* The cache is pruned on finalization and bounds the number of roots stored per proposer and slot
|
|
20
20
|
*/
|
|
21
21
|
export class SeenBlockProposers {
|
|
22
|
-
private readonly proposerIndexesBySlot = new MapDef<Slot,
|
|
22
|
+
private readonly proposerIndexesBySlot = new MapDef<Slot, Map<ValidatorIndex, RootHex>>(
|
|
23
|
+
() => new Map<ValidatorIndex, RootHex>()
|
|
24
|
+
);
|
|
23
25
|
private readonly signedBlockHeadersBySlot = new MapDef<
|
|
24
26
|
Slot,
|
|
25
27
|
MapDef<ValidatorIndex, Map<RootHex, phase0.SignedBeaconBlockHeader>>
|
|
@@ -30,6 +32,14 @@ export class SeenBlockProposers {
|
|
|
30
32
|
return this.proposerIndexesBySlot.get(blockSlot)?.has(proposerIndex) === true;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The block proposer is known at slot with a different root.
|
|
37
|
+
*/
|
|
38
|
+
isRepeatProposal(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): boolean {
|
|
39
|
+
const knownRoot = this.proposerIndexesBySlot.get(blockSlot)?.get(proposerIndex);
|
|
40
|
+
return knownRoot !== undefined && knownRoot !== blockRoot;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
hasBlockRoot(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): boolean {
|
|
34
44
|
return this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.has(blockRoot) === true;
|
|
35
45
|
}
|
|
@@ -85,12 +95,12 @@ export class SeenBlockProposers {
|
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
/** Mark a block as known from gossip or another block import path */
|
|
88
|
-
add(blockSlot: Slot, proposerIndex: ValidatorIndex): void {
|
|
98
|
+
add(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): void {
|
|
89
99
|
if (blockSlot < this.finalizedSlot) {
|
|
90
100
|
throw Error(`blockSlot ${blockSlot} < finalizedSlot ${this.finalizedSlot}`);
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
this.proposerIndexesBySlot.getOrDefault(blockSlot).
|
|
103
|
+
this.proposerIndexesBySlot.getOrDefault(blockSlot).set(proposerIndex, blockRoot);
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
prune(finalizedSlot: Slot): void {
|
|
@@ -89,13 +89,20 @@ export async function validateGossipBlock(
|
|
|
89
89
|
|
|
90
90
|
// [IGNORE] The block is the first block with valid signature received for the proposer for the slot, signed_beacon_block.message.slot.
|
|
91
91
|
const proposerIndex = block.proposerIndex;
|
|
92
|
-
const hasBlockRoot = chain.seenBlockProposers.hasBlockRoot(blockSlot, proposerIndex, blockRoot);
|
|
93
92
|
if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
chain.seenBlockProposers.
|
|
93
|
+
if (chain.seenBlockProposers.isRepeatProposal(blockSlot, proposerIndex, blockRoot)) {
|
|
94
|
+
const hasBlockRoot = chain.seenBlockProposers.hasBlockRoot(blockSlot, proposerIndex, blockRoot);
|
|
95
|
+
if (!hasBlockRoot && !chain.seenBlockProposers.isEquivocating(blockSlot, proposerIndex)) {
|
|
96
|
+
await verifyBlockProposerSignature(chain, signedBlock, blockRoot, {verifyOnMainThread: false});
|
|
97
|
+
chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader);
|
|
98
|
+
}
|
|
99
|
+
throw new BlockGossipError(GossipAction.IGNORE, {
|
|
100
|
+
code: BlockErrorCode.REPEAT_PROPOSAL,
|
|
101
|
+
proposerIndex,
|
|
102
|
+
root: blockRoot,
|
|
103
|
+
});
|
|
97
104
|
}
|
|
98
|
-
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.
|
|
105
|
+
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot});
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
// [REJECT] The current finalized_checkpoint is an ancestor of block -- i.e.
|
|
@@ -301,10 +308,17 @@ export async function validateGossipBlock(
|
|
|
301
308
|
|
|
302
309
|
// Check again after all async validation and the early-block delay so concurrent proposals cannot both pass
|
|
303
310
|
if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
|
|
304
|
-
|
|
311
|
+
if (chain.seenBlockProposers.isRepeatProposal(blockSlot, proposerIndex, blockRoot)) {
|
|
312
|
+
throw new BlockGossipError(GossipAction.IGNORE, {
|
|
313
|
+
code: BlockErrorCode.REPEAT_PROPOSAL,
|
|
314
|
+
proposerIndex,
|
|
315
|
+
root: blockRoot,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot});
|
|
305
319
|
}
|
|
306
320
|
|
|
307
|
-
chain.seenBlockProposers.add(blockSlot, proposerIndex);
|
|
321
|
+
chain.seenBlockProposers.add(blockSlot, proposerIndex, blockRoot);
|
|
308
322
|
|
|
309
323
|
return {skippedSlots};
|
|
310
324
|
}
|
|
@@ -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
|
|
|
@@ -713,9 +713,32 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
713
713
|
const {serializedData} = gossipData;
|
|
714
714
|
|
|
715
715
|
const signedBlock = sszDeserialize(topic, serializedData);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
716
|
+
try {
|
|
717
|
+
const blockInput = await validateBeaconBlock(signedBlock, topic.boundary.fork, peerIdStr, seenTimestampSec);
|
|
718
|
+
chain.serializedCache.set(signedBlock, serializedData);
|
|
719
|
+
handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
|
|
720
|
+
} catch (e) {
|
|
721
|
+
// Spec: IGNORE the block, ie not to re-publish to peers
|
|
722
|
+
// but we should still import an equivocating (REPEAT_PROPOSAL) block into fork choice because we don't
|
|
723
|
+
// know if this block (or the 1st known block with same slot) will become canonical yet
|
|
724
|
+
if (
|
|
725
|
+
e instanceof BlockGossipError &&
|
|
726
|
+
e.type.code === BlockErrorCode.REPEAT_PROPOSAL &&
|
|
727
|
+
// this is make sure the block's proposer signature was verified, it should be true anyway
|
|
728
|
+
chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root)
|
|
729
|
+
) {
|
|
730
|
+
// blockInput was optimistically seeded in validateBeaconBlock and retained on IGNORE
|
|
731
|
+
const blockInput = chain.seenBlockInputCache.get(e.type.root);
|
|
732
|
+
if (blockInput) {
|
|
733
|
+
chain.serializedCache.set(signedBlock, serializedData);
|
|
734
|
+
// this is technically not a valid gossip block but gossip validation is a cheap subset of checks
|
|
735
|
+
// this runs the full state transition, so importing an equivocating-but-valid block here is safe.
|
|
736
|
+
handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// rethrow so gossipValidatorFn maps IGNORE -> TopicValidatorResult.Ignore (message not forwarded)
|
|
740
|
+
throw e;
|
|
741
|
+
}
|
|
719
742
|
},
|
|
720
743
|
|
|
721
744
|
[GossipType.blob_sidecar]: async ({
|
package/src/sync/unknownBlock.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {routes} from "@lodestar/api";
|
|
2
2
|
import {ChainForkConfig} from "@lodestar/config";
|
|
3
|
-
import {ForkSeq} from "@lodestar/params";
|
|
3
|
+
import {ForkSeq, isForkPostGloas} from "@lodestar/params";
|
|
4
4
|
import {RequestError, RequestErrorCode} from "@lodestar/reqresp";
|
|
5
5
|
import {computeTimeAtSlot} from "@lodestar/state-transition";
|
|
6
6
|
import {RootHex, Slot, gloas} from "@lodestar/types";
|
|
@@ -764,9 +764,10 @@ export class BlockInputSync {
|
|
|
764
764
|
// this prevents unbundling attack
|
|
765
765
|
// see https://lighthouse-blog.sigmaprime.io/mev-unbundling-rpc.html
|
|
766
766
|
const {slot: blockSlot, proposerIndex} = pendingBlock.blockInput.getBlock().message;
|
|
767
|
+
const blockRootHex = pendingBlock.blockInput.blockRootHex;
|
|
767
768
|
const logCtx = {
|
|
768
769
|
slot: blockSlot,
|
|
769
|
-
root:
|
|
770
|
+
root: blockRootHex,
|
|
770
771
|
delaySec: this.chain.clock.secFromSlot(blockSlot),
|
|
771
772
|
};
|
|
772
773
|
this.logger.verbose("Processing downloaded block", logCtx);
|
|
@@ -780,7 +781,7 @@ export class BlockInputSync {
|
|
|
780
781
|
// eligible for proposer boost to prevent unbundling attack
|
|
781
782
|
this.logger.verbose("Avoid proposer boost for this block of known proposer", {
|
|
782
783
|
slot: blockSlot,
|
|
783
|
-
root:
|
|
784
|
+
root: blockRootHex,
|
|
784
785
|
proposerIndex,
|
|
785
786
|
});
|
|
786
787
|
await sleep(proposerBoostWindowMs);
|
|
@@ -807,12 +808,24 @@ export class BlockInputSync {
|
|
|
807
808
|
if (!res.err) {
|
|
808
809
|
this.logger.verbose("Processed block from unknown sync", logCtx);
|
|
809
810
|
// no need to update status to "processed", delete anyway
|
|
810
|
-
this.pendingBlocks.delete(
|
|
811
|
+
this.pendingBlocks.delete(blockRootHex);
|
|
812
|
+
|
|
813
|
+
if (isForkPostGloas(fork)) {
|
|
814
|
+
const payloadInput = this.chain.seenPayloadEnvelopeInputCache.get(blockRootHex);
|
|
815
|
+
if (!payloadInput) {
|
|
816
|
+
this.logger.warn("PayloadEnvelopeInput not seeded during unknown sync processReadyBlock()", logCtx);
|
|
817
|
+
} else {
|
|
818
|
+
// Similar to the gossip path: immediately attempt fetch of data columns from the execution engine.
|
|
819
|
+
// The bid already carries the kzg commitments, so there is no reason to wait for the payload to arrive.
|
|
820
|
+
this.chain.getBlobsTracker.triggerGetBlobs(payloadInput);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
811
824
|
// Re-enter the scheduler so descendants blocked on either parent blocks or parent payloads
|
|
812
825
|
// are advanced through the same dependency checks as every other pending item.
|
|
813
826
|
this.triggerUnknownBlockSearch();
|
|
814
827
|
} else {
|
|
815
|
-
const errorData = {slot: pendingBlock.blockInput.slot, root:
|
|
828
|
+
const errorData = {slot: pendingBlock.blockInput.slot, root: blockRootHex};
|
|
816
829
|
if (res.err instanceof BlockError) {
|
|
817
830
|
switch (res.err.type.code) {
|
|
818
831
|
// This cases are already handled with `{ignoreIfKnown: true}`
|