@lodestar/beacon-node 1.48.0-dev.b5f4a8f2c6 → 1.48.0-dev.b6386729a9
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/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/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 +10 -1
- 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/blockError.d.ts +3 -5
- package/lib/chain/errors/blockError.d.ts.map +1 -1
- package/lib/chain/errors/blockError.js +2 -2
- package/lib/chain/errors/blockError.js.map +1 -1
- package/lib/chain/errors/executionPayloadEnvelope.d.ts +0 -11
- package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
- package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
- package/lib/chain/errors/executionPayloadEnvelope.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/prepareNextSlot.js +13 -13
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +46 -64
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/validation/block.d.ts.map +1 -1
- package/lib/chain/validation/block.js +9 -50
- package/lib/chain/validation/block.js.map +1 -1
- package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
- package/lib/chain/validation/executionPayloadEnvelope.js +0 -29
- package/lib/chain/validation/executionPayloadEnvelope.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/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/network/processor/index.d.ts.map +1 -1
- package/lib/network/processor/index.js +11 -0
- 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 +15 -15
- package/src/api/impl/beacon/state/utils.ts +15 -0
- 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 +10 -1
- package/src/chain/chain.ts +78 -50
- package/src/chain/emitter.ts +1 -1
- package/src/chain/errors/blockError.ts +3 -3
- package/src/chain/errors/executionPayloadEnvelope.ts +0 -9
- 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/prepareNextSlot.ts +16 -16
- package/src/chain/produceBlock/produceBlockBody.ts +50 -71
- package/src/chain/validation/block.ts +9 -66
- package/src/chain/validation/executionPayloadEnvelope.ts +0 -37
- package/src/chain/validatorMonitor.ts +24 -7
- package/src/execution/engine/mock.ts +4 -1
- package/src/network/processor/index.ts +13 -0
- package/src/network/reqresp/rateLimit.ts +2 -1
- package/src/sync/range/range.ts +23 -1
|
@@ -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(
|
|
@@ -48,6 +48,15 @@ export function assertLinearChainSegment(
|
|
|
48
48
|
if (parentBlock !== null) {
|
|
49
49
|
const parentPayloadInput = payloadEnvelopes?.get(parentBlock.slot);
|
|
50
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
|
+
}
|
|
51
60
|
const parentPayloadBlockHash = parentPayloadInput.getBlockHashHex();
|
|
52
61
|
if (currentExecHash === parentPayloadBlockHash) {
|
|
53
62
|
// Checkpoint-sync first batch: the anchor is stored PENDING with the inherited (grandparent)
|
package/src/chain/chain.ts
CHANGED
|
@@ -82,7 +82,7 @@ import {IBlockInput, isBlockInputBlobs, isBlockInputColumns} from "./blocks/bloc
|
|
|
82
82
|
import {BlockProcessor, ImportBlockOpts} from "./blocks/index.js";
|
|
83
83
|
import {PayloadEnvelopeInputSource} from "./blocks/payloadEnvelopeInput/index.js";
|
|
84
84
|
import {PayloadEnvelopeProcessor} from "./blocks/payloadEnvelopeProcessor.js";
|
|
85
|
-
import {ImportPayloadOpts} from "./blocks/types.js";
|
|
85
|
+
import {ImportPayloadOpts, ProcessBlocksResult} from "./blocks/types.js";
|
|
86
86
|
import {persistBlockInput} from "./blocks/writeBlockInputToDb.js";
|
|
87
87
|
import {persistPayloadEnvelopeInput} from "./blocks/writePayloadEnvelopeInputToDb.js";
|
|
88
88
|
import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "./bls/index.js";
|
|
@@ -1167,15 +1167,15 @@ export class BeaconChain implements IBeaconChain {
|
|
|
1167
1167
|
}
|
|
1168
1168
|
|
|
1169
1169
|
async processBlock(block: IBlockInput, opts?: ImportBlockOpts): Promise<void> {
|
|
1170
|
-
|
|
1170
|
+
await this.blockProcessor.processBlocksJob([block], null, opts);
|
|
1171
1171
|
}
|
|
1172
1172
|
|
|
1173
1173
|
async processChainSegment(
|
|
1174
1174
|
blocks: IBlockInput[],
|
|
1175
1175
|
payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
|
|
1176
1176
|
opts?: ImportBlockOpts
|
|
1177
|
-
): Promise<
|
|
1178
|
-
|
|
1177
|
+
): Promise<ProcessBlocksResult> {
|
|
1178
|
+
return this.blockProcessor.processBlocksJob(blocks, payloadEnvelopes, opts);
|
|
1179
1179
|
}
|
|
1180
1180
|
|
|
1181
1181
|
async processExecutionPayload(payloadInput: PayloadEnvelopeInput, opts?: ImportPayloadOpts): Promise<void> {
|
|
@@ -1271,59 +1271,65 @@ export class BeaconChain implements IBeaconChain {
|
|
|
1271
1271
|
try {
|
|
1272
1272
|
const prevHead = this.forkChoice.getHead();
|
|
1273
1273
|
const head = this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head;
|
|
1274
|
+
this.emitHeadEvents(prevHead, head);
|
|
1275
|
+
return head;
|
|
1276
|
+
} catch (e) {
|
|
1277
|
+
this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
|
|
1278
|
+
throw e;
|
|
1279
|
+
} finally {
|
|
1280
|
+
timer?.();
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1274
1283
|
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1284
|
+
/**
|
|
1285
|
+
* Emit `head` and `head_v2` when the fork choice head differs between two observations. Every caller that
|
|
1286
|
+
* can move the head, `updateAndGetHead()` and `updateTime()`, must capture the head before and pass both.
|
|
1287
|
+
*/
|
|
1288
|
+
private emitHeadEvents(prevHead: ProtoBlock, head: ProtoBlock): void {
|
|
1289
|
+
const headRootChanged = head.blockRoot !== prevHead.blockRoot;
|
|
1280
1290
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const epochTransition = computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot;
|
|
1285
|
-
const executionOptimistic = isOptimisticBlock(head);
|
|
1286
|
-
|
|
1287
|
-
if (headRootChanged) {
|
|
1288
|
-
this.emitter.emit(routes.events.EventType.head, {
|
|
1289
|
-
block: head.blockRoot,
|
|
1290
|
-
epochTransition,
|
|
1291
|
-
slot: head.slot,
|
|
1292
|
-
state: head.stateRoot,
|
|
1293
|
-
previousDutyDependentRoot,
|
|
1294
|
-
currentDutyDependentRoot,
|
|
1295
|
-
executionOptimistic,
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1291
|
+
if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1298
1294
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1295
|
+
try {
|
|
1296
|
+
const previousDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.previous);
|
|
1297
|
+
const currentDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.current);
|
|
1298
|
+
const epochTransition = computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot;
|
|
1299
|
+
const executionOptimistic = isOptimisticBlock(head);
|
|
1300
|
+
|
|
1301
|
+
if (headRootChanged) {
|
|
1302
|
+
this.emitter.emit(routes.events.EventType.head, {
|
|
1303
|
+
block: head.blockRoot,
|
|
1304
|
+
epochTransition,
|
|
1305
|
+
slot: head.slot,
|
|
1306
|
+
state: head.stateRoot,
|
|
1307
|
+
previousDutyDependentRoot,
|
|
1308
|
+
currentDutyDependentRoot,
|
|
1309
|
+
executionOptimistic,
|
|
1311
1310
|
});
|
|
1312
|
-
} catch (e) {
|
|
1313
|
-
// getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
|
|
1314
|
-
this.logger.debug(
|
|
1315
|
-
"Error emitting head/head_v2 event",
|
|
1316
|
-
{slot: head.slot, root: head.blockRoot, headRootChanged},
|
|
1317
|
-
e as Error
|
|
1318
|
-
);
|
|
1319
1311
|
}
|
|
1320
1312
|
|
|
1321
|
-
|
|
1313
|
+
this.emitter.emit(routes.events.EventType.headV2, {
|
|
1314
|
+
version: this.config.getForkName(head.slot),
|
|
1315
|
+
data: {
|
|
1316
|
+
slot: head.slot,
|
|
1317
|
+
block: head.blockRoot,
|
|
1318
|
+
state: head.stateRoot,
|
|
1319
|
+
payloadStatus: head.payloadStatus === PayloadStatus.FULL ? "full" : "empty",
|
|
1320
|
+
epochTransition,
|
|
1321
|
+
currentEpochDependentRoot: previousDutyDependentRoot,
|
|
1322
|
+
nextEpochDependentRoot: currentDutyDependentRoot,
|
|
1323
|
+
executionOptimistic,
|
|
1324
|
+
},
|
|
1325
|
+
});
|
|
1322
1326
|
} catch (e) {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
+
// getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
|
|
1328
|
+
this.logger.debug(
|
|
1329
|
+
"Error emitting head/head_v2 event",
|
|
1330
|
+
{slot: head.slot, root: head.blockRoot, headRootChanged},
|
|
1331
|
+
e as Error
|
|
1332
|
+
);
|
|
1327
1333
|
}
|
|
1328
1334
|
}
|
|
1329
1335
|
|
|
@@ -1612,7 +1618,10 @@ export class BeaconChain implements IBeaconChain {
|
|
|
1612
1618
|
this.processShutdownCallback(this.forkChoice.irrecoverableError);
|
|
1613
1619
|
}
|
|
1614
1620
|
|
|
1621
|
+
// updateTime() recomputes the head on epoch-boundary checkpoint pull-up and under fast confirmation
|
|
1622
|
+
const prevHead = this.forkChoice.getHead();
|
|
1615
1623
|
this.forkChoice.updateTime(slot);
|
|
1624
|
+
this.emitHeadEvents(prevHead, this.forkChoice.getHead());
|
|
1616
1625
|
this.metrics?.clockSlot.set(slot);
|
|
1617
1626
|
|
|
1618
1627
|
this.attestationPool.prune(slot);
|
|
@@ -1670,6 +1679,25 @@ export class BeaconChain implements IBeaconChain {
|
|
|
1670
1679
|
|
|
1671
1680
|
private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise<void> {
|
|
1672
1681
|
this.logger.verbose("Fork choice finalized", {epoch: cp.epoch, root: cp.rootHex});
|
|
1682
|
+
this.metrics?.finalizedEpoch.set(cp.epoch);
|
|
1683
|
+
const finalizedBlock = this.forkChoice.getBlockHexDefaultStatus(cp.rootHex);
|
|
1684
|
+
if (finalizedBlock) {
|
|
1685
|
+
// The callback runs synchronously inside fork choice checkpoint updates, defer writing to subscribers
|
|
1686
|
+
callInNextEventLoop(() => {
|
|
1687
|
+
this.emitter.emit(routes.events.EventType.finalizedCheckpoint, {
|
|
1688
|
+
block: cp.rootHex,
|
|
1689
|
+
epoch: cp.epoch,
|
|
1690
|
+
state: finalizedBlock.stateRoot,
|
|
1691
|
+
executionOptimistic: isOptimisticBlock(finalizedBlock),
|
|
1692
|
+
});
|
|
1693
|
+
});
|
|
1694
|
+
} else {
|
|
1695
|
+
this.logger.debug("Finalized block not found in fork choice, skip finalized_checkpoint event", {
|
|
1696
|
+
epoch: cp.epoch,
|
|
1697
|
+
root: cp.rootHex,
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1673
1701
|
const finalizedSlot = computeStartSlotAtEpoch(cp.epoch);
|
|
1674
1702
|
this.seenBlockProposers.prune(finalizedSlot);
|
|
1675
1703
|
|
package/src/chain/emitter.ts
CHANGED
|
@@ -33,7 +33,7 @@ export enum ChainEvent {
|
|
|
33
33
|
/**
|
|
34
34
|
* This event signals that the fork choice store has been updated.
|
|
35
35
|
*
|
|
36
|
-
* This event is guaranteed to be triggered whenever the fork choice
|
|
36
|
+
* This event is guaranteed to be triggered whenever the fork choice finalized checkpoint is updated. This is either in response to a newly processed block or a new clock tick.
|
|
37
37
|
*/
|
|
38
38
|
forkChoiceFinalized = "forkChoice:finalized",
|
|
39
39
|
/**
|
|
@@ -78,8 +78,8 @@ export enum BlockErrorCode {
|
|
|
78
78
|
TOO_MANY_KZG_COMMITMENTS = "BLOCK_ERROR_TOO_MANY_KZG_COMMITMENTS",
|
|
79
79
|
/** Bid parent block root does not match block parent root */
|
|
80
80
|
BID_PARENT_ROOT_MISMATCH = "BLOCK_ERROR_BID_PARENT_ROOT_MISMATCH",
|
|
81
|
-
/** A block
|
|
82
|
-
|
|
81
|
+
/** A block contains deposits where none are allowed */
|
|
82
|
+
NON_ZERO_DEPOSITS = "BLOCK_ERROR_NON_ZERO_DEPOSITS",
|
|
83
83
|
/** The parent block's execution payload has been verified as invalid */
|
|
84
84
|
PARENT_EXECUTION_INVALID = "BLOCK_ERROR_PARENT_EXECUTION_INVALID",
|
|
85
85
|
/**
|
|
@@ -153,7 +153,7 @@ export type BlockErrorType =
|
|
|
153
153
|
| {code: BlockErrorCode.DATA_UNAVAILABLE}
|
|
154
154
|
| {code: BlockErrorCode.TOO_MANY_KZG_COMMITMENTS; blobKzgCommitmentsLen: number; commitmentLimit: number}
|
|
155
155
|
| {code: BlockErrorCode.BID_PARENT_ROOT_MISMATCH; bidParentRoot: RootHex; blockParentRoot: RootHex}
|
|
156
|
-
| {code: BlockErrorCode.
|
|
156
|
+
| {code: BlockErrorCode.NON_ZERO_DEPOSITS; count: number}
|
|
157
157
|
| {code: BlockErrorCode.PARENT_EXECUTION_INVALID; parentRoot: RootHex}
|
|
158
158
|
| {code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN; parentRoot: RootHex; parentBlockHash: RootHex}
|
|
159
159
|
| {code: BlockErrorCode.NON_LINEAR_PAYLOAD_ROOTS; parentBlockHash: RootHex; expectedBlockHash: RootHex};
|
|
@@ -12,8 +12,6 @@ export enum ExecutionPayloadEnvelopeErrorCode {
|
|
|
12
12
|
BUILDER_INDEX_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BUILDER_INDEX_MISMATCH",
|
|
13
13
|
BLOCK_HASH_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_HASH_MISMATCH",
|
|
14
14
|
EXECUTION_REQUESTS_ROOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_ROOT_MISMATCH",
|
|
15
|
-
EXECUTION_REQUESTS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_COUNT_EXCEEDED",
|
|
16
|
-
WITHDRAWALS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_WITHDRAWALS_COUNT_EXCEEDED",
|
|
17
15
|
INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE",
|
|
18
16
|
PAYLOAD_ENVELOPE_INPUT_MISSING = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_PAYLOAD_ENVELOPE_INPUT_MISSING",
|
|
19
17
|
}
|
|
@@ -44,13 +42,6 @@ export type ExecutionPayloadEnvelopeErrorType =
|
|
|
44
42
|
envelopeRequestsRoot: RootHex;
|
|
45
43
|
bidRequestsRoot: RootHex;
|
|
46
44
|
}
|
|
47
|
-
| {
|
|
48
|
-
code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_COUNT_EXCEEDED;
|
|
49
|
-
name: string;
|
|
50
|
-
count: number;
|
|
51
|
-
limit: number;
|
|
52
|
-
}
|
|
53
|
-
| {code: ExecutionPayloadEnvelopeErrorCode.WITHDRAWALS_COUNT_EXCEEDED; count: number; limit: number}
|
|
54
45
|
| {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE}
|
|
55
46
|
| {code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING; blockRoot: RootHex};
|
|
56
47
|
|
|
@@ -8,6 +8,7 @@ export enum GossipAction {
|
|
|
8
8
|
export enum GossipErrorCode {
|
|
9
9
|
INVALID_SERIALIZED_BYTES_ERROR_CODE = "GOSSIP_ERROR_INVALID_SERIALIZED_BYTES",
|
|
10
10
|
PAST_SLOT = "GOSSIP_ERROR_PAST_SLOT",
|
|
11
|
+
FUTURE_SLOT = "GOSSIP_ERROR_FUTURE_SLOT",
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export class GossipActionError<T extends {code: string}> extends LodestarError<T> {
|
package/src/chain/interface.ts
CHANGED
|
@@ -35,7 +35,7 @@ import {IArchiveStore} from "./archiveStore/interface.js";
|
|
|
35
35
|
import {CheckpointBalancesCache} from "./balancesCache.js";
|
|
36
36
|
import {BeaconProposerCache, ProposerPreparationData} from "./beaconProposerCache.js";
|
|
37
37
|
import {IBlockInput} from "./blocks/blockInput/index.js";
|
|
38
|
-
import {ImportBlockOpts, ImportPayloadOpts} from "./blocks/types.js";
|
|
38
|
+
import {ImportBlockOpts, ImportPayloadOpts, ProcessBlocksResult} from "./blocks/types.js";
|
|
39
39
|
import {IBlsVerifier} from "./bls/index.js";
|
|
40
40
|
import {BuilderCircuitBreaker} from "./builderCircuitBreaker.js";
|
|
41
41
|
import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js";
|
|
@@ -262,7 +262,7 @@ export interface IBeaconChain {
|
|
|
262
262
|
blocks: IBlockInput[],
|
|
263
263
|
payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null,
|
|
264
264
|
opts?: ImportBlockOpts
|
|
265
|
-
): Promise<
|
|
265
|
+
): Promise<ProcessBlocksResult>;
|
|
266
266
|
|
|
267
267
|
/** Process execution payload envelope: verify, import to fork choice, and persist to DB */
|
|
268
268
|
processExecutionPayload(payloadInput: PayloadEnvelopeInput, opts?: ImportPayloadOpts): Promise<void>;
|
|
@@ -363,7 +363,7 @@ export class AggregatedAttestationPool {
|
|
|
363
363
|
stateEpoch,
|
|
364
364
|
rootCache,
|
|
365
365
|
gloasState?.executionPayloadAvailability ?? null,
|
|
366
|
-
gloasState?.
|
|
366
|
+
gloasState?.latestBlockHeader.slot ?? null
|
|
367
367
|
);
|
|
368
368
|
|
|
369
369
|
const weight =
|
|
@@ -243,6 +243,22 @@ export class PrepareNextSlotScheduler {
|
|
|
243
243
|
parentBlockHash = preparedState.latestExecutionPayloadHeader.blockHash;
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
// If emitPayloadAttributes is true emit a SSE payloadAttributes event for
|
|
247
|
+
// every slot. Without the flag, only emit the event if we are proposing in the next slot.
|
|
248
|
+
if (
|
|
249
|
+
(feeRecipient || this.chain.opts.emitPayloadAttributes === true) &&
|
|
250
|
+
this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)
|
|
251
|
+
) {
|
|
252
|
+
const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
|
|
253
|
+
prepareState: stateAfterParentPayload,
|
|
254
|
+
prepareSlot,
|
|
255
|
+
parentBlockRoot: fromHex(updatedHead.blockRoot),
|
|
256
|
+
parentBlockHash,
|
|
257
|
+
feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
|
|
258
|
+
});
|
|
259
|
+
this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
|
|
260
|
+
}
|
|
261
|
+
|
|
246
262
|
if (feeRecipient) {
|
|
247
263
|
const preparationTime =
|
|
248
264
|
computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
|
|
@@ -273,22 +289,6 @@ export class PrepareNextSlotScheduler {
|
|
|
273
289
|
}
|
|
274
290
|
|
|
275
291
|
this.computeStateHashTreeRoot(preparedState, isEpochTransition);
|
|
276
|
-
|
|
277
|
-
// If emitPayloadAttributes is true emit a SSE payloadAttributes event for
|
|
278
|
-
// every slot. Without the flag, only emit the event if we are proposing in the next slot.
|
|
279
|
-
if (
|
|
280
|
-
(feeRecipient || this.chain.opts.emitPayloadAttributes === true) &&
|
|
281
|
-
this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)
|
|
282
|
-
) {
|
|
283
|
-
const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
|
|
284
|
-
prepareState: stateAfterParentPayload,
|
|
285
|
-
prepareSlot,
|
|
286
|
-
parentBlockRoot: fromHex(updatedHead.blockRoot),
|
|
287
|
-
parentBlockHash,
|
|
288
|
-
feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
|
|
289
|
-
});
|
|
290
|
-
this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
|
|
291
|
-
}
|
|
292
292
|
} else {
|
|
293
293
|
// Pre-bellatrix only reaches here at an epoch transition to precompute the next epoch state
|
|
294
294
|
const preparedState = await this.chain.regen.getBlockSlotState(
|