@lodestar/beacon-node 1.48.0-dev.dd957b093d → 1.48.0-dev.df6a2aa429
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 -7
- package/lib/api/impl/beacon/blocks/index.js.map +1 -1
- package/lib/api/impl/beacon/state/utils.d.ts.map +1 -1
- package/lib/api/impl/beacon/state/utils.js +14 -0
- package/lib/api/impl/beacon/state/utils.js.map +1 -1
- package/lib/api/impl/config/index.d.ts.map +1 -1
- package/lib/api/impl/config/index.js +4 -1
- package/lib/api/impl/config/index.js.map +1 -1
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +39 -39
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/blocks/importBlock.d.ts +5 -1
- package/lib/chain/blocks/importBlock.d.ts.map +1 -1
- package/lib/chain/blocks/importBlock.js +9 -16
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/index.d.ts +8 -4
- package/lib/chain/blocks/index.d.ts.map +1 -1
- package/lib/chain/blocks/index.js +80 -15
- package/lib/chain/blocks/index.js.map +1 -1
- package/lib/chain/blocks/types.d.ts +8 -0
- package/lib/chain/blocks/types.d.ts.map +1 -1
- package/lib/chain/blocks/types.js.map +1 -1
- package/lib/chain/blocks/utils/chainSegment.d.ts +1 -1
- package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
- package/lib/chain/blocks/utils/chainSegment.js +26 -11
- package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
- package/lib/chain/chain.d.ts +7 -2
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +69 -40
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/emitter.d.ts +1 -1
- package/lib/chain/emitter.js +1 -1
- package/lib/chain/errors/executionPayloadBid.d.ts +4 -0
- package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/errors/executionPayloadBid.js +1 -0
- package/lib/chain/errors/executionPayloadBid.js.map +1 -1
- package/lib/chain/errors/gossipValidation.d.ts +2 -1
- package/lib/chain/errors/gossipValidation.d.ts.map +1 -1
- package/lib/chain/errors/gossipValidation.js +1 -0
- package/lib/chain/errors/gossipValidation.js.map +1 -1
- package/lib/chain/interface.d.ts +2 -2
- package/lib/chain/interface.d.ts.map +1 -1
- package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
- package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
- package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/validation/executionPayloadBid.js +16 -0
- package/lib/chain/validation/executionPayloadBid.js.map +1 -1
- package/lib/chain/validatorMonitor.d.ts +7 -1
- package/lib/chain/validatorMonitor.d.ts.map +1 -1
- package/lib/chain/validatorMonitor.js +22 -5
- package/lib/chain/validatorMonitor.js.map +1 -1
- package/lib/execution/builder/validateBid.d.ts.map +1 -1
- package/lib/execution/builder/validateBid.js +3 -0
- package/lib/execution/builder/validateBid.js.map +1 -1
- package/lib/execution/engine/mock.d.ts.map +1 -1
- package/lib/execution/engine/mock.js +4 -1
- package/lib/execution/engine/mock.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +0 -3
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +0 -5
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +378 -278
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/network/processor/index.d.ts.map +1 -1
- package/lib/network/processor/index.js +22 -16
- package/lib/network/processor/index.js.map +1 -1
- package/lib/network/reqresp/rateLimit.js +2 -1
- package/lib/network/reqresp/rateLimit.js.map +1 -1
- package/lib/sync/range/range.d.ts.map +1 -1
- package/lib/sync/range/range.js +24 -1
- package/lib/sync/range/range.js.map +1 -1
- package/package.json +14 -14
- package/src/api/impl/beacon/blocks/index.ts +0 -10
- package/src/api/impl/beacon/state/utils.ts +15 -0
- package/src/api/impl/config/index.ts +4 -1
- package/src/api/impl/validator/index.ts +44 -43
- package/src/chain/blocks/importBlock.ts +21 -18
- package/src/chain/blocks/index.ts +107 -21
- package/src/chain/blocks/types.ts +9 -0
- package/src/chain/blocks/utils/chainSegment.ts +26 -12
- package/src/chain/chain.ts +78 -50
- package/src/chain/emitter.ts +1 -1
- package/src/chain/errors/executionPayloadBid.ts +2 -0
- package/src/chain/errors/gossipValidation.ts +1 -0
- package/src/chain/interface.ts +2 -2
- package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
- package/src/chain/validation/executionPayloadBid.ts +18 -0
- package/src/chain/validatorMonitor.ts +24 -7
- package/src/execution/builder/validateBid.ts +4 -0
- package/src/execution/engine/mock.ts +4 -1
- package/src/metrics/metrics/lodestar.ts +0 -5
- package/src/network/processor/gossipHandlers.ts +487 -348
- package/src/network/processor/index.ts +24 -17
- package/src/network/reqresp/rateLimit.ts +2 -1
- package/src/sync/range/range.ts +23 -1
|
@@ -1073,16 +1073,6 @@ export function getBeaconBlockApi({
|
|
|
1073
1073
|
const elapsedSec = chain.clock.secFromSlot(slot, seenTimestampSec);
|
|
1074
1074
|
metrics?.gossipExecutionPayloadBid.elapsedTimeTillReceived.observe({source: OpSource.api}, elapsedSec);
|
|
1075
1075
|
|
|
1076
|
-
try {
|
|
1077
|
-
const insertOutcome = chain.executionPayloadBidPool.add(
|
|
1078
|
-
signedExecutionPayloadBid,
|
|
1079
|
-
Math.floor(elapsedSec * 1000)
|
|
1080
|
-
);
|
|
1081
|
-
metrics?.opPool.executionPayloadBidPool.apiInsertOutcome.inc({insertOutcome});
|
|
1082
|
-
} catch (e) {
|
|
1083
|
-
chain.logger.error("Error adding to executionPayloadBid pool", {}, e as Error);
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
1076
|
const sentPeers = await network.publishSignedExecutionPayloadBid(signedExecutionPayloadBid);
|
|
1087
1077
|
|
|
1088
1078
|
chain.emitter.emit(routes.events.EventType.executionPayloadBid, {
|
|
@@ -15,7 +15,9 @@ import {
|
|
|
15
15
|
} from "@lodestar/types";
|
|
16
16
|
import {byteArrayEquals, fromHex} from "@lodestar/utils";
|
|
17
17
|
import {IBeaconChain} from "../../../../chain/index.js";
|
|
18
|
+
import {GENESIS_EPOCH} from "../../../../constants/index.js";
|
|
18
19
|
import {IBeaconSync} from "../../../../sync/index.js";
|
|
20
|
+
import {isOptimisticBlock} from "../../../../util/forkChoice.js";
|
|
19
21
|
import {ApiError, ValidationError} from "../../errors.js";
|
|
20
22
|
import {notWhileSyncing} from "../../utils.js";
|
|
21
23
|
|
|
@@ -65,6 +67,19 @@ export async function getStateResponseWithRegen(
|
|
|
65
67
|
notWhileSyncing(chain, sync.state);
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
if (inStateId === "head") {
|
|
71
|
+
const head = chain.forkChoice.getHead();
|
|
72
|
+
// Head block's post-state is not cached after init from an anchor state at a skipped boundary slot,
|
|
73
|
+
// it does not exist anywhere and regen has no seed for it, serve the closest head state instead
|
|
74
|
+
const state = chain.regen.getStateSync(head.stateRoot) ?? chain.getHeadState();
|
|
75
|
+
const finalizedEpoch = chain.forkChoice.getFinalizedCheckpoint().epoch;
|
|
76
|
+
return {
|
|
77
|
+
state,
|
|
78
|
+
executionOptimistic: isOptimisticBlock(head),
|
|
79
|
+
finalized: state.epoch <= finalizedEpoch && finalizedEpoch !== GENESIS_EPOCH,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
const stateId = resolveStateId(chain.forkChoice, inStateId);
|
|
69
84
|
|
|
70
85
|
const res =
|
|
@@ -20,11 +20,14 @@ export function renderJsonSpec(config: ChainConfig): routes.config.Spec {
|
|
|
20
20
|
const presetJson = presetToJson(activePreset);
|
|
21
21
|
const constantsJson = specValuesToJson(specConstants);
|
|
22
22
|
|
|
23
|
-
// TODO Fulu: remove
|
|
23
|
+
// TODO Fulu/Gloas: remove these checks once interop issues are resolved
|
|
24
24
|
// see https://github.com/attestantio/go-eth2-client/issues/230
|
|
25
25
|
if (config.FULU_FORK_EPOCH === Infinity) {
|
|
26
26
|
delete configJson.BLOB_SCHEDULE;
|
|
27
27
|
}
|
|
28
|
+
if (config.GLOAS_FORK_EPOCH === Infinity) {
|
|
29
|
+
delete configJson.GAS_LIMIT_SCHEDULE;
|
|
30
|
+
}
|
|
28
31
|
|
|
29
32
|
return {...configJson, ...presetJson, ...constantsJson};
|
|
30
33
|
}
|
|
@@ -133,40 +133,37 @@ type BidCandidate = {
|
|
|
133
133
|
};
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* p2p bid, and the earlier received bid between builder api bids.
|
|
136
|
+
* Ranking value of a bid, the boosted total payment scaled by 100 as `boostFactor` is a percentage.
|
|
137
|
+
* Kept scaled so bids that differ by less than a percent are not truncated into a tie.
|
|
139
138
|
*/
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (candidateIsMaxBoost !== bestIsMaxBoost) {
|
|
152
|
-
if (candidateIsMaxBoost) {
|
|
153
|
-
best = candidate;
|
|
154
|
-
}
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const candidateValue = boostedValue(candidate);
|
|
158
|
-
const bestValue = boostedValue(best);
|
|
159
|
-
if (candidateValue > bestValue) {
|
|
160
|
-
best = candidate;
|
|
161
|
-
} else if (
|
|
162
|
-
candidateValue === bestValue &&
|
|
163
|
-
// A tie prefers a builder api bid over the p2p bid, and the earlier received bid otherwise
|
|
164
|
-
(best.url === undefined || (candidate.url !== undefined && candidate.receivedMs < best.receivedMs))
|
|
165
|
-
) {
|
|
166
|
-
best = candidate;
|
|
167
|
-
}
|
|
139
|
+
function getBoostedTotalScaled({totalGwei, boostFactor}: BidCandidate): bigint {
|
|
140
|
+
return boostFactor * totalGwei;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Order bid candidates by preference, best first, used to select a bid and to log the ranking */
|
|
144
|
+
function compareBidCandidates(a: BidCandidate, b: BidCandidate): number {
|
|
145
|
+
// Preserve max boost preference before comparing bid values
|
|
146
|
+
const aIsMaxBoost = a.boostFactor === MAX_BUILDER_BOOST_FACTOR;
|
|
147
|
+
const bIsMaxBoost = b.boostFactor === MAX_BUILDER_BOOST_FACTOR;
|
|
148
|
+
if (aIsMaxBoost !== bIsMaxBoost) {
|
|
149
|
+
return aIsMaxBoost ? -1 : 1;
|
|
168
150
|
}
|
|
169
|
-
|
|
151
|
+
|
|
152
|
+
const aBoostedTotal = getBoostedTotalScaled(a);
|
|
153
|
+
const bBoostedTotal = getBoostedTotalScaled(b);
|
|
154
|
+
if (aBoostedTotal !== bBoostedTotal) {
|
|
155
|
+
return aBoostedTotal > bBoostedTotal ? -1 : 1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A tie prefers a builder api bid over the p2p bid
|
|
159
|
+
const aIsBuilderApi = a.url !== undefined;
|
|
160
|
+
const bIsBuilderApi = b.url !== undefined;
|
|
161
|
+
if (aIsBuilderApi !== bIsBuilderApi) {
|
|
162
|
+
return aIsBuilderApi ? -1 : 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// and the earlier received bid between builder api bids
|
|
166
|
+
return a.receivedMs - b.receivedMs;
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
type ProduceBlockContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Wei} & {
|
|
@@ -1049,20 +1046,24 @@ export function getValidatorApi(
|
|
|
1049
1046
|
});
|
|
1050
1047
|
}
|
|
1051
1048
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1049
|
+
if (candidates.length === 0) {
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const rankedCandidates = candidates.toSorted(compareBidCandidates);
|
|
1054
|
+
for (const [index, candidate] of rankedCandidates.entries()) {
|
|
1055
|
+
logger.debug("Builder bid candidate", {
|
|
1055
1056
|
slot,
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1057
|
+
rank: index + 1,
|
|
1058
|
+
source: candidate.url !== undefined ? toPrintableUrl(candidate.url) : "p2p",
|
|
1059
|
+
builder: candidate.signedBid.message.builderIndex,
|
|
1060
|
+
total: prettyGweiToEth(candidate.totalGwei),
|
|
1061
|
+
boost: candidate.boostFactor,
|
|
1062
|
+
boosted: prettyGweiToEth(getBoostedTotalScaled(candidate) / 100n),
|
|
1063
|
+
receivedMs: candidate.receivedMs,
|
|
1063
1064
|
});
|
|
1064
1065
|
}
|
|
1065
|
-
return
|
|
1066
|
+
return rankedCandidates[0];
|
|
1066
1067
|
})();
|
|
1067
1068
|
|
|
1068
1069
|
const commonBlockBodyPromise = chain.produceCommonBlockBody({
|
|
@@ -25,7 +25,17 @@ import {
|
|
|
25
25
|
isStatePostAltair,
|
|
26
26
|
isStatePostBellatrix,
|
|
27
27
|
} from "@lodestar/state-transition";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
Attestation,
|
|
30
|
+
BeaconBlock,
|
|
31
|
+
Epoch,
|
|
32
|
+
altair,
|
|
33
|
+
capella,
|
|
34
|
+
electra,
|
|
35
|
+
isGloasBeaconBlock,
|
|
36
|
+
phase0,
|
|
37
|
+
ssz,
|
|
38
|
+
} from "@lodestar/types";
|
|
29
39
|
import {isErrorAborted, toRootHex} from "@lodestar/utils";
|
|
30
40
|
import {GENESIS_SLOT, ZERO_HASH_HEX} from "../../constants/index.js";
|
|
31
41
|
import {callInNextEventLoop} from "../../util/eventLoop.js";
|
|
@@ -44,6 +54,15 @@ import {getCheckpointFromState} from "./utils/checkpoint.js";
|
|
|
44
54
|
* Fork-choice allows to import attestations from current (0) or past (1) epoch.
|
|
45
55
|
*/
|
|
46
56
|
const FORK_CHOICE_ATT_EPOCH_LIMIT = 1;
|
|
57
|
+
/**
|
|
58
|
+
* Whether the attestations of a block at `blockEpoch` are imported into fork choice
|
|
59
|
+
*/
|
|
60
|
+
export function importsBlockAttestations(opts: ImportBlockOpts, blockEpoch: Epoch, currentEpoch: Epoch): boolean {
|
|
61
|
+
return (
|
|
62
|
+
opts.importAttestations === AttestationImportOpt.Force ||
|
|
63
|
+
(opts.importAttestations !== AttestationImportOpt.Skip && blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT)
|
|
64
|
+
);
|
|
65
|
+
}
|
|
47
66
|
/**
|
|
48
67
|
* Emit eventstream events for block contents events only for blocks that are recent enough to clock
|
|
49
68
|
*/
|
|
@@ -149,10 +168,7 @@ export async function importBlock(
|
|
|
149
168
|
// Only process attestations of blocks with relevant attestations for the fork-choice:
|
|
150
169
|
// If current epoch is N, and block is epoch X, block may include attestations for epoch X or X - 1.
|
|
151
170
|
// The latest block that is useful is at epoch N - 1 which may include attestations for epoch N - 1 or N - 2.
|
|
152
|
-
if (
|
|
153
|
-
opts.importAttestations === AttestationImportOpt.Force ||
|
|
154
|
-
(opts.importAttestations !== AttestationImportOpt.Skip && blockEpoch >= currentEpoch - FORK_CHOICE_ATT_EPOCH_LIMIT)
|
|
155
|
-
) {
|
|
171
|
+
if (importsBlockAttestations(opts, blockEpoch, currentEpoch)) {
|
|
156
172
|
const attestations = block.message.body.attestations;
|
|
157
173
|
const rootCache = new RootCache(postState);
|
|
158
174
|
const invalidAttestationErrorsByCode = new Map<string, {error: Error; count: number}>();
|
|
@@ -508,19 +524,6 @@ export async function importBlock(
|
|
|
508
524
|
this.metrics?.previousJustifiedEpoch.set(checkpointState.previousJustifiedCheckpoint.epoch);
|
|
509
525
|
this.metrics?.currentJustifiedEpoch.set(justifiedCheckpoint.epoch);
|
|
510
526
|
}
|
|
511
|
-
const finalizedCheckpoint = checkpointState.finalizedCheckpoint;
|
|
512
|
-
const finalizedEpoch = finalizedCheckpoint.epoch;
|
|
513
|
-
const preFinalizedEpoch = parentBlockSummary.finalizedEpoch;
|
|
514
|
-
if (finalizedEpoch > preFinalizedEpoch) {
|
|
515
|
-
this.emitter.emit(routes.events.EventType.finalizedCheckpoint, {
|
|
516
|
-
block: toRootHex(finalizedCheckpoint.root),
|
|
517
|
-
epoch: finalizedCheckpoint.epoch,
|
|
518
|
-
state: toRootHex(checkpointState.hashTreeRoot()),
|
|
519
|
-
executionOptimistic: false,
|
|
520
|
-
});
|
|
521
|
-
this.logger.verbose("Checkpoint finalized", toCheckpointHex(finalizedCheckpoint));
|
|
522
|
-
this.metrics?.finalizedEpoch.set(finalizedCheckpoint.epoch);
|
|
523
|
-
}
|
|
524
527
|
}
|
|
525
528
|
}
|
|
526
529
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {PayloadStatus} from "@lodestar/fork-choice";
|
|
2
|
+
import {computeEpochAtSlot} from "@lodestar/state-transition";
|
|
1
3
|
import {SignedBeaconBlock, Slot} from "@lodestar/types";
|
|
2
4
|
import {isErrorAborted, toRootHex} from "@lodestar/utils";
|
|
3
5
|
import {Metrics} from "../../metrics/metrics.js";
|
|
@@ -5,15 +7,17 @@ import {nextEventLoop} from "../../util/eventLoop.js";
|
|
|
5
7
|
import {JobItemQueue, isQueueErrorAborted} from "../../util/queue/index.js";
|
|
6
8
|
import type {BeaconChain} from "../chain.js";
|
|
7
9
|
import {BlockError, BlockErrorCode, isBlockErrorAborted} from "../errors/index.js";
|
|
10
|
+
import {ForkchoiceCaller} from "../forkChoice/index.js";
|
|
8
11
|
import {BlockProcessOpts} from "../options.js";
|
|
9
12
|
import {IBlockInput} from "./blockInput/types.js";
|
|
10
|
-
import {importBlock} from "./importBlock.js";
|
|
13
|
+
import {importBlock, importsBlockAttestations} from "./importBlock.js";
|
|
11
14
|
import {PayloadError, importExecutionPayload} from "./importExecutionPayload.js";
|
|
12
15
|
import {PayloadEnvelopeInput} from "./payloadEnvelopeInput/payloadEnvelopeInput.js";
|
|
13
|
-
import {FullyVerifiedBlock, ImportBlockOpts} from "./types.js";
|
|
14
|
-
import {assertLinearChainSegment} from "./utils/chainSegment.js";
|
|
16
|
+
import {FullyVerifiedBlock, ImportBlockOpts, ProcessBlocksResult} from "./types.js";
|
|
17
|
+
import {OrphanedPayloadEnvelope, assertLinearChainSegment} from "./utils/chainSegment.js";
|
|
15
18
|
import {verifyBlocksInEpoch} from "./verifyBlock.js";
|
|
16
19
|
import {verifyBlocksSanityChecks} from "./verifyBlocksSanityChecks.js";
|
|
20
|
+
import {verifyPayloadsDataAvailability} from "./verifyPayloadsDataAvailability.js";
|
|
17
21
|
|
|
18
22
|
export {AttestationImportOpt, type ImportBlockOpts} from "./types.js";
|
|
19
23
|
|
|
@@ -23,10 +27,16 @@ const QUEUE_MAX_LENGTH = 256;
|
|
|
23
27
|
* BlockProcessor processes block jobs in a queued fashion, one after the other.
|
|
24
28
|
*/
|
|
25
29
|
export class BlockProcessor {
|
|
26
|
-
readonly jobQueue: JobItemQueue<
|
|
30
|
+
readonly jobQueue: JobItemQueue<
|
|
31
|
+
[IBlockInput[], Map<Slot, PayloadEnvelopeInput> | null, ImportBlockOpts],
|
|
32
|
+
ProcessBlocksResult
|
|
33
|
+
>;
|
|
27
34
|
|
|
28
35
|
constructor(chain: BeaconChain, metrics: Metrics | null, opts: BlockProcessOpts, signal: AbortSignal) {
|
|
29
|
-
this.jobQueue = new JobItemQueue<
|
|
36
|
+
this.jobQueue = new JobItemQueue<
|
|
37
|
+
[IBlockInput[], Map<Slot, PayloadEnvelopeInput> | null, ImportBlockOpts],
|
|
38
|
+
ProcessBlocksResult
|
|
39
|
+
>(
|
|
30
40
|
(job, payloadEnvelopes, importOpts) => {
|
|
31
41
|
return processBlocks.call(chain, job, payloadEnvelopes, {...opts, ...importOpts});
|
|
32
42
|
},
|
|
@@ -39,8 +49,8 @@ export class BlockProcessor {
|
|
|
39
49
|
job: IBlockInput[],
|
|
40
50
|
payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
|
|
41
51
|
opts: ImportBlockOpts = {}
|
|
42
|
-
): Promise<
|
|
43
|
-
|
|
52
|
+
): Promise<ProcessBlocksResult> {
|
|
53
|
+
return this.jobQueue.push(job, payloadEnvelopes, opts);
|
|
44
54
|
}
|
|
45
55
|
}
|
|
46
56
|
|
|
@@ -59,9 +69,10 @@ export async function processBlocks(
|
|
|
59
69
|
blocks: IBlockInput[],
|
|
60
70
|
payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
|
|
61
71
|
opts: BlockProcessOpts & ImportBlockOpts
|
|
62
|
-
): Promise<
|
|
72
|
+
): Promise<ProcessBlocksResult> {
|
|
73
|
+
const emptyResult: ProcessBlocksResult = {orphaned: [], skipped: false};
|
|
63
74
|
if (blocks.length === 0) {
|
|
64
|
-
return; // TODO: or throw?
|
|
75
|
+
return emptyResult; // TODO: or throw?
|
|
65
76
|
}
|
|
66
77
|
|
|
67
78
|
try {
|
|
@@ -70,7 +81,7 @@ export async function processBlocks(
|
|
|
70
81
|
// No relevant blocks, skip verifyBlocksInEpoch()
|
|
71
82
|
if (relevantBlocks.length === 0 || parentBlock === null) {
|
|
72
83
|
// parentBlock can only be null if relevantBlocks are empty
|
|
73
|
-
return;
|
|
84
|
+
return await importPayloadEnvelopesOfKnownBlocks.call(this, payloadEnvelopes, opts);
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
const {warnings: orphanedPayloads} = assertLinearChainSegment(
|
|
@@ -79,12 +90,21 @@ export async function processBlocks(
|
|
|
79
90
|
payloadEnvelopes,
|
|
80
91
|
parentBlock
|
|
81
92
|
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
// Orphaned payloads of recent blocks are still imported, a reorg of the child could make the payload canonical
|
|
94
|
+
// again and later blocks build on it. Older ones are skipped: without the block attestations the orphaned FULL
|
|
95
|
+
// variant has no descendants and ties at zero weight with the EMPTY variant carrying the chain, the payload
|
|
96
|
+
// status tiebreaker then picks FULL and parks the head there.
|
|
97
|
+
const blockEpoch = computeEpochAtSlot(relevantBlocks[0].getBlock().message.slot);
|
|
98
|
+
const importsAttestations = importsBlockAttestations(opts, blockEpoch, this.clock.currentEpoch);
|
|
99
|
+
const skipped = orphanedPayloads != null && payloadEnvelopes !== null && !importsAttestations;
|
|
100
|
+
let payloadEnvelopesToImport = payloadEnvelopes;
|
|
101
|
+
if (skipped) {
|
|
102
|
+
payloadEnvelopesToImport = new Map(payloadEnvelopes);
|
|
103
|
+
for (const orphaned of orphanedPayloads ?? []) {
|
|
104
|
+
payloadEnvelopesToImport.delete(orphaned.slot);
|
|
105
|
+
// never validated, drop it from the shared cache so it is not served to peers, by-root sync reloads the
|
|
106
|
+
// entry from db if the payload is ever needed
|
|
107
|
+
this.seenPayloadEnvelopeInputCache.prune(orphaned.payloadEnvelopeInput.blockRootHex);
|
|
88
108
|
}
|
|
89
109
|
}
|
|
90
110
|
|
|
@@ -97,7 +117,7 @@ export async function processBlocks(
|
|
|
97
117
|
proposerBalanceDeltas,
|
|
98
118
|
segmentExecStatus,
|
|
99
119
|
indexedAttestationsByBlock,
|
|
100
|
-
} = await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks,
|
|
120
|
+
} = await verifyBlocksInEpoch.call(this, parentBlock, relevantBlocks, payloadEnvelopesToImport, opts);
|
|
101
121
|
|
|
102
122
|
// If segmentExecStatus has lvhForkchoice then, the entire segment should be invalid
|
|
103
123
|
// and we need to further propagate
|
|
@@ -132,8 +152,8 @@ export async function processBlocks(
|
|
|
132
152
|
}
|
|
133
153
|
|
|
134
154
|
const slotSet = new Set<Slot>(blocks.map((b) => b.getBlock().message.slot));
|
|
135
|
-
if (
|
|
136
|
-
for (const slot of
|
|
155
|
+
if (payloadEnvelopesToImport) {
|
|
156
|
+
for (const slot of payloadEnvelopesToImport.keys()) slotSet.add(slot);
|
|
137
157
|
}
|
|
138
158
|
const slots = Array.from(slotSet).sort((a, b) => a - b);
|
|
139
159
|
for (const slot of slots) {
|
|
@@ -146,7 +166,7 @@ export async function processBlocks(
|
|
|
146
166
|
// PayloadEnvelopeInput is shared and may receive an envelope after the DA snapshot was taken.
|
|
147
167
|
const payloadDA = payloadDAStatuses.get(slot);
|
|
148
168
|
if (payloadDA !== undefined) {
|
|
149
|
-
const payloadInput =
|
|
169
|
+
const payloadInput = payloadEnvelopesToImport?.get(slot);
|
|
150
170
|
if (payloadInput === undefined) {
|
|
151
171
|
throw new Error(`Missing payload input for slot ${slot} after DA verification`);
|
|
152
172
|
}
|
|
@@ -159,9 +179,11 @@ export async function processBlocks(
|
|
|
159
179
|
|
|
160
180
|
await nextEventLoop();
|
|
161
181
|
}
|
|
182
|
+
|
|
183
|
+
return {orphaned: orphanedPayloads ?? [], skipped};
|
|
162
184
|
} catch (e) {
|
|
163
185
|
if (isErrorAborted(e) || isQueueErrorAborted(e) || isBlockErrorAborted(e)) {
|
|
164
|
-
return; // Ignore
|
|
186
|
+
return emptyResult; // Ignore
|
|
165
187
|
}
|
|
166
188
|
|
|
167
189
|
// above functions should only throw BlockError, or PayloadError from the gloas payload import
|
|
@@ -209,6 +231,70 @@ export async function processBlocks(
|
|
|
209
231
|
}
|
|
210
232
|
}
|
|
211
233
|
|
|
234
|
+
/**
|
|
235
|
+
* A peer can serve a block without its envelope, the block is then imported with a PENDING payload. Later batches
|
|
236
|
+
* carry the envelope but all their blocks are known, without importing it here every child building on the FULL
|
|
237
|
+
* variant fails with PARENT_PAYLOAD_UNKNOWN and range sync never progresses. If our head already descends from the
|
|
238
|
+
* block's EMPTY variant the chain did not build on the payload, importing it would only add a dead FULL leaf. As for
|
|
239
|
+
* orphaned payloads in a segment this only applies while the block attestations are not imported, a recent payload
|
|
240
|
+
* is imported anyway so a reorg building on it does not have to fetch it through unknown block sync.
|
|
241
|
+
*/
|
|
242
|
+
async function importPayloadEnvelopesOfKnownBlocks(
|
|
243
|
+
this: BeaconChain,
|
|
244
|
+
payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
|
|
245
|
+
opts: ImportBlockOpts
|
|
246
|
+
): Promise<ProcessBlocksResult> {
|
|
247
|
+
if (payloadEnvelopes === null) {
|
|
248
|
+
return {orphaned: [], skipped: false};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const head = this.forkChoice.getHead();
|
|
252
|
+
const currentEpoch = this.clock.currentEpoch;
|
|
253
|
+
const payloadInputs: PayloadEnvelopeInput[] = [];
|
|
254
|
+
const orphaned: OrphanedPayloadEnvelope[] = [];
|
|
255
|
+
for (const payloadInput of payloadEnvelopes.values()) {
|
|
256
|
+
const {blockRootHex, slot} = payloadInput;
|
|
257
|
+
if (
|
|
258
|
+
!payloadInput.hasPayloadEnvelope() ||
|
|
259
|
+
!this.forkChoice.hasBlockHex(blockRootHex) ||
|
|
260
|
+
this.forkChoice.getBlockHexAndBlockHash(blockRootHex, payloadInput.getBlockHashHex()) !== null
|
|
261
|
+
) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (
|
|
265
|
+
head.blockRoot !== blockRootHex &&
|
|
266
|
+
!importsBlockAttestations(opts, computeEpochAtSlot(slot), currentEpoch) &&
|
|
267
|
+
this.forkChoice.isDescendant(blockRootHex, PayloadStatus.EMPTY, head.blockRoot, head.payloadStatus)
|
|
268
|
+
) {
|
|
269
|
+
// never validated, see processBlocks()
|
|
270
|
+
this.seenPayloadEnvelopeInputCache.prune(blockRootHex);
|
|
271
|
+
orphaned.push({slot, payloadEnvelopeInput: payloadInput});
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
payloadInputs.push(payloadInput);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// orphaned envelopes on this path are always skipped, range sync logs them with peer/client context
|
|
278
|
+
const result: ProcessBlocksResult = {orphaned, skipped: orphaned.length > 0};
|
|
279
|
+
|
|
280
|
+
if (payloadInputs.length === 0) {
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(payloadInputs, new AbortController().signal);
|
|
285
|
+
for (let i = 0; i < payloadInputs.length; i++) {
|
|
286
|
+
this.logger.debug("Importing payload envelope of known block", {
|
|
287
|
+
slot: payloadInputs[i].slot,
|
|
288
|
+
blockRoot: payloadInputs[i].blockRootHex,
|
|
289
|
+
});
|
|
290
|
+
await importExecutionPayload.call(this, payloadInputs[i], dataAvailabilityStatuses[i], {validSignature: false});
|
|
291
|
+
}
|
|
292
|
+
// the new FULL variants are not seen by the head cached from the last block import
|
|
293
|
+
this.recomputeForkChoiceHead(ForkchoiceCaller.importBlock);
|
|
294
|
+
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
212
298
|
function getBlockOrPayloadError(e: unknown, block: SignedBeaconBlock): BlockError | PayloadError {
|
|
213
299
|
if (e instanceof BlockError) {
|
|
214
300
|
return e;
|
|
@@ -4,6 +4,7 @@ import {ForkSeq} from "@lodestar/params";
|
|
|
4
4
|
import {DataAvailabilityStatus, IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition";
|
|
5
5
|
import type {IndexedAttestation, Slot, fulu} from "@lodestar/types";
|
|
6
6
|
import {IBlockInput} from "./blockInput/types.js";
|
|
7
|
+
import {OrphanedPayloadEnvelope} from "./utils/chainSegment.js";
|
|
7
8
|
|
|
8
9
|
export enum GossipedInputType {
|
|
9
10
|
block = "block",
|
|
@@ -110,3 +111,11 @@ export type FullyVerifiedBlock = {
|
|
|
110
111
|
/** If the execution payload couldn't be verified because of EL syncing status, used in optimistic sync */
|
|
111
112
|
executionStatus: BlockExecutionStatus | PayloadExecutionStatus;
|
|
112
113
|
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Result of processing a chain segment.
|
|
117
|
+
*/
|
|
118
|
+
export type ProcessBlocksResult = {
|
|
119
|
+
orphaned: OrphanedPayloadEnvelope[];
|
|
120
|
+
skipped: boolean;
|
|
121
|
+
};
|
|
@@ -22,7 +22,7 @@ export type ChainSegmentResult = {warnings: OrphanedPayloadEnvelope[] | null};
|
|
|
22
22
|
* - Verifies parent root + slot linearity
|
|
23
23
|
* - For gloas: verifies bid.parentBlockHash matches the tracked execution hash; if not, the
|
|
24
24
|
* previous FULL envelope is treated as orphaned (segment continues as if previous slot was EMPTY)
|
|
25
|
-
* - If an envelope exists for this slot: verifies it references this block's root
|
|
25
|
+
* - If an envelope exists for this slot (or the parent's slot): verifies it references this block's root
|
|
26
26
|
* - Advances the tracked execution hash (FULL if envelope present, EMPTY if not)
|
|
27
27
|
*/
|
|
28
28
|
export function assertLinearChainSegment(
|
|
@@ -37,17 +37,6 @@ export function assertLinearChainSegment(
|
|
|
37
37
|
|
|
38
38
|
const warnings: OrphanedPayloadEnvelope[] = [];
|
|
39
39
|
|
|
40
|
-
let parentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
|
|
41
|
-
if (parentBlock !== null) {
|
|
42
|
-
const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
|
|
43
|
-
if (parentPayloadInput?.hasPayloadEnvelope()) {
|
|
44
|
-
// Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
|
|
45
|
-
// hash, not its own payload. Its payload envelope is in this batch, so use that blockHash as
|
|
46
|
-
// the real parent EL head.
|
|
47
|
-
parentExecHash = parentPayloadInput.getBlockHashHex();
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
40
|
// Track the expected execution payload block hash through the segment, seeded from the FIRST
|
|
52
41
|
// block's own bid
|
|
53
42
|
const firstBlockMessage = blocks[0].getBlock().message;
|
|
@@ -55,6 +44,31 @@ export function assertLinearChainSegment(
|
|
|
55
44
|
? toRootHex(firstBlockMessage.body.signedExecutionPayloadBid.message.parentBlockHash)
|
|
56
45
|
: null;
|
|
57
46
|
|
|
47
|
+
let parentExecHash: string | null = parentBlock?.executionPayloadBlockHash ?? null;
|
|
48
|
+
if (parentBlock !== null) {
|
|
49
|
+
const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
|
|
50
|
+
if (parentPayloadInput?.hasPayloadEnvelope()) {
|
|
51
|
+
// Envelopes are served by slot, the one at the parent's slot must reference the parent block. Without this
|
|
52
|
+
// check an envelope of a sibling block is taken as orphaned and only fails downstream in the payload import
|
|
53
|
+
if (parentPayloadInput.blockRootHex !== parentBlock.blockRoot) {
|
|
54
|
+
throw new BlockError(blocks[0].getBlock(), {
|
|
55
|
+
code: BlockErrorCode.ENVELOPE_BLOCK_ROOT_MISMATCH,
|
|
56
|
+
envelopeBlockRoot: parentPayloadInput.blockRootHex,
|
|
57
|
+
blockRoot: parentBlock.blockRoot,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const parentPayloadBlockHash = parentPayloadInput.getBlockHashHex();
|
|
61
|
+
if (currentExecHash === parentPayloadBlockHash) {
|
|
62
|
+
// Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
|
|
63
|
+
// hash, its own payload is in this batch and is the real parent EL head
|
|
64
|
+
parentExecHash = parentPayloadBlockHash;
|
|
65
|
+
} else {
|
|
66
|
+
// First block builds on the parent's EMPTY variant, the parent's payload in this batch is orphaned
|
|
67
|
+
warnings.push({slot: parentBlock.slot, payloadEnvelopeInput: parentPayloadInput});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
// First block must build on the parent's EL head. PARENT_PAYLOAD_UNKNOWN (vs the in-segment
|
|
59
73
|
// NON_LINEAR_PAYLOAD_ROOTS below) tells range sync this is a parent-link problem, not an in-batch one.
|
|
60
74
|
if (parentExecHash !== null && currentExecHash !== null && currentExecHash !== parentExecHash) {
|