@lodestar/beacon-node 1.51.0-dev.e91a5043ed → 1.51.0-dev.f39fad29a1
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/api.js +1 -1
- package/lib/api/impl/api.js.map +1 -1
- package/lib/api/impl/beacon/blocks/index.d.ts +2 -1
- package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/blocks/index.js +12 -7
- package/lib/api/impl/beacon/blocks/index.js.map +1 -1
- package/lib/api/impl/beacon/index.d.ts +2 -1
- package/lib/api/impl/beacon/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/index.js +2 -2
- package/lib/api/impl/beacon/index.js.map +1 -1
- package/lib/api/options.d.ts +2 -0
- package/lib/api/options.d.ts.map +1 -1
- package/lib/api/options.js +1 -0
- package/lib/api/options.js.map +1 -1
- package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
- package/lib/chain/archiveStore/utils/archiveBlocks.js +1 -58
- package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
- package/lib/chain/regen/interface.d.ts +0 -1
- package/lib/chain/regen/interface.d.ts.map +1 -1
- package/lib/chain/regen/interface.js +0 -1
- package/lib/chain/regen/interface.js.map +1 -1
- package/lib/chain/validation/executionPayloadBid.d.ts +0 -15
- package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/validation/executionPayloadBid.js +0 -153
- package/lib/chain/validation/executionPayloadBid.js.map +1 -1
- package/lib/db/beacon.d.ts.map +1 -1
- package/lib/db/beacon.js +2 -0
- package/lib/db/beacon.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +3 -0
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +5 -0
- 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 +27 -18
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +42 -18
- package/lib/sync/unknownBlock.js.map +1 -1
- package/package.json +15 -15
- package/src/api/impl/api.ts +1 -1
- package/src/api/impl/beacon/blocks/index.ts +22 -17
- package/src/api/impl/beacon/index.ts +3 -1
- package/src/api/options.ts +3 -0
- package/src/chain/archiveStore/utils/archiveBlocks.ts +1 -84
- package/src/chain/regen/interface.ts +0 -1
- package/src/chain/validation/executionPayloadBid.ts +0 -180
- package/src/db/beacon.ts +5 -0
- package/src/index.ts +2 -0
- package/src/metrics/metrics/lodestar.ts +5 -0
- package/src/network/processor/gossipHandlers.ts +35 -20
- package/src/sync/unknownBlock.ts +47 -28
|
@@ -3,6 +3,7 @@ import {ApplicationMethods} from "@lodestar/api/server";
|
|
|
3
3
|
import {isForkPostGloas} from "@lodestar/params";
|
|
4
4
|
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
5
5
|
import {toRootHex} from "@lodestar/utils";
|
|
6
|
+
import {ApiOptions} from "../../options.js";
|
|
6
7
|
import {ApiError} from "../errors.js";
|
|
7
8
|
import {ApiModules} from "../types.js";
|
|
8
9
|
import {getBeaconBlockApi} from "./blocks/index.js";
|
|
@@ -11,9 +12,10 @@ import {getBeaconRewardsApi} from "./rewards/index.js";
|
|
|
11
12
|
import {getBeaconStateApi} from "./state/index.js";
|
|
12
13
|
|
|
13
14
|
export function getBeaconApi(
|
|
15
|
+
opts: ApiOptions,
|
|
14
16
|
modules: Pick<ApiModules, "chain" | "config" | "logger" | "metrics" | "network" | "db" | "sync">
|
|
15
17
|
): ApplicationMethods<routes.beacon.Endpoints> {
|
|
16
|
-
const block = getBeaconBlockApi(modules);
|
|
18
|
+
const block = getBeaconBlockApi(opts, modules);
|
|
17
19
|
const pool = getBeaconPoolApi(modules);
|
|
18
20
|
const state = getBeaconStateApi(modules);
|
|
19
21
|
const rewards = getBeaconRewardsApi(modules);
|
package/src/api/options.ts
CHANGED
|
@@ -2,6 +2,8 @@ import {BeaconRestApiServerOpts, beaconRestApiServerOpts} from "./rest/index.js"
|
|
|
2
2
|
|
|
3
3
|
export type ApiOptions = {
|
|
4
4
|
maxGindicesInProof?: number;
|
|
5
|
+
/** Broadcast execution payload bids submitted via the API without running gossip validation */
|
|
6
|
+
disableBidValidation?: boolean;
|
|
5
7
|
rest: BeaconRestApiServerOpts;
|
|
6
8
|
commit?: string;
|
|
7
9
|
version?: string;
|
|
@@ -10,6 +12,7 @@ export type ApiOptions = {
|
|
|
10
12
|
|
|
11
13
|
export const defaultApiOptions: ApiOptions = {
|
|
12
14
|
maxGindicesInProof: 512,
|
|
15
|
+
disableBidValidation: false,
|
|
13
16
|
rest: beaconRestApiServerOpts,
|
|
14
17
|
version: "dev",
|
|
15
18
|
private: false,
|
|
@@ -5,7 +5,7 @@ import {CheckpointWithHex, ExecutionStatus, IForkChoice, PayloadStatus, ProtoBlo
|
|
|
5
5
|
import {ForkSeq, SLOTS_PER_EPOCH} from "@lodestar/params";
|
|
6
6
|
import {computeEpochAtSlot, computeStartSlotAtEpoch, getBlobDataRetentionStartEpoch} from "@lodestar/state-transition";
|
|
7
7
|
import {Epoch, Slot} from "@lodestar/types";
|
|
8
|
-
import {Logger,
|
|
8
|
+
import {Logger, fromHex, prettyPrintIndices, toRootHex} from "@lodestar/utils";
|
|
9
9
|
import {IBeaconDb} from "../../../db/index.js";
|
|
10
10
|
import {BlockArchiveBatchPutBinaryItem, encodeArchivedHeaderEnvelope} from "../../../db/repositories/index.js";
|
|
11
11
|
import {Metrics} from "../../../metrics/metrics.js";
|
|
@@ -136,23 +136,6 @@ export async function archiveBlocks(
|
|
|
136
136
|
logger.verbose("Migrated blobSidecars from hot DB to cold DB", {...logCtx, migratedEntries});
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
// Keep the normal LevelDB hot-to-cold flow for legacy columns. New columns are written
|
|
140
|
-
// directly to flat files, so this only moves fallback data left by older versions.
|
|
141
|
-
if (finalizedPostFulu) {
|
|
142
|
-
const migratedSlots = await migrateDataColumnSidecarsFromHotToColdDb(
|
|
143
|
-
config,
|
|
144
|
-
db,
|
|
145
|
-
logger,
|
|
146
|
-
finalizedCanonicalBlocks,
|
|
147
|
-
currentEpoch
|
|
148
|
-
);
|
|
149
|
-
logger.verbose("Migrated legacy dataColumnSidecars from hot DB to cold DB", {
|
|
150
|
-
...logCtx,
|
|
151
|
-
migratedEntries: migratedSlots.length,
|
|
152
|
-
slotRange: prettyPrintIndices(migratedSlots),
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
|
|
156
139
|
if (finalizedPostGloas) {
|
|
157
140
|
const migratedSlots = await migrateExecutionPayloadEnvelopesFromHotToColdDb(
|
|
158
141
|
config,
|
|
@@ -408,72 +391,6 @@ async function migrateBlobSidecarsFromHotToColdDb(
|
|
|
408
391
|
return migratedWrappedBlobSidecars;
|
|
409
392
|
}
|
|
410
393
|
|
|
411
|
-
// TODO: This function can be simplified further by reducing layers of promises in a loop
|
|
412
|
-
/**
|
|
413
|
-
* Post-gloas the data columns of a Gloas block are tied to its execution payload envelope.
|
|
414
|
-
* Columns only exist once the FULL variant of the block is in the proto-array. Pre-Gloas
|
|
415
|
-
* blocks only have a FULL variant, so the payload-status filter passes them all.
|
|
416
|
-
*/
|
|
417
|
-
async function migrateDataColumnSidecarsFromHotToColdDb(
|
|
418
|
-
config: ChainForkConfig,
|
|
419
|
-
db: IBeaconDb,
|
|
420
|
-
logger: Logger,
|
|
421
|
-
canonicalBlocks: ProtoBlock[],
|
|
422
|
-
currentEpoch: Epoch
|
|
423
|
-
): Promise<Slot[]> {
|
|
424
|
-
const columnBlocks = canonicalBlocks.filter(
|
|
425
|
-
(block) => config.getForkSeq(block.slot) < ForkSeq.gloas || block.payloadStatus === PayloadStatus.FULL
|
|
426
|
-
);
|
|
427
|
-
if (columnBlocks.length === 0) return [];
|
|
428
|
-
const blocks: BlockRootSlot[] = columnBlocks.map((block) => ({slot: block.slot, root: fromHex(block.blockRoot)}));
|
|
429
|
-
|
|
430
|
-
const migratedSlots: Slot[] = [];
|
|
431
|
-
for (let i = 0; i < blocks.length; i += BLOB_SIDECAR_BATCH_SIZE) {
|
|
432
|
-
const toIdx = Math.min(i + BLOB_SIDECAR_BATCH_SIZE, blocks.length);
|
|
433
|
-
const batch = blocks.slice(i, toIdx);
|
|
434
|
-
if (batch.length === 0) break;
|
|
435
|
-
|
|
436
|
-
const promises: Promise<void>[] = [];
|
|
437
|
-
for (const block of batch) {
|
|
438
|
-
const blockEpoch = computeEpochAtSlot(block.slot);
|
|
439
|
-
if (
|
|
440
|
-
config.getForkSeq(block.slot) < ForkSeq.fulu ||
|
|
441
|
-
blockEpoch < getBlobDataRetentionStartEpoch(config, currentEpoch)
|
|
442
|
-
) {
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const dataColumnSidecarBytes = await fromAsync(db.dataColumnSidecar.valuesStreamBinary(block.root));
|
|
447
|
-
if (dataColumnSidecarBytes.length === 0) {
|
|
448
|
-
logger.debug("DataColumnSidecars in forkchoice but missing in hot db, could be already archived", {
|
|
449
|
-
slot: block.slot,
|
|
450
|
-
root: toRootHex(block.root),
|
|
451
|
-
});
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
logger.verbose("Migrated legacy dataColumnSidecars for block", {
|
|
455
|
-
currentEpoch,
|
|
456
|
-
slot: block.slot,
|
|
457
|
-
root: toRootHex(block.root),
|
|
458
|
-
numSidecars: dataColumnSidecarBytes.length,
|
|
459
|
-
});
|
|
460
|
-
promises.push(
|
|
461
|
-
db.dataColumnSidecarArchive.putManyBinary(
|
|
462
|
-
block.slot,
|
|
463
|
-
dataColumnSidecarBytes.map((entry) => ({key: entry.id, value: entry.value}))
|
|
464
|
-
)
|
|
465
|
-
);
|
|
466
|
-
migratedSlots.push(block.slot);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
await Promise.all(promises);
|
|
470
|
-
await db.dataColumnSidecar.deleteMany(batch.map((block) => block.root));
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Ancestor walk is newest to oldest; sort ascending so `prettyPrintIndices` renders cleanly.
|
|
474
|
-
return migratedSlots.sort((a, b) => a - b);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
394
|
/**
|
|
478
395
|
* Post-gloas given a finalized checkpoint at a block root, payload of that block root
|
|
479
396
|
* is not considered finalized, hence they are archived in the next run.
|
|
@@ -25,7 +25,6 @@ export enum RegenCaller {
|
|
|
25
25
|
publishDeferredVoluntaryExits = "publishDeferredVoluntaryExits",
|
|
26
26
|
validateGossipExecutionPayloadBid = "validateGossipExecutionPayloadBid",
|
|
27
27
|
validateGossipInclusionList = "validateGossipInclusionList",
|
|
28
|
-
validateApiExecutionPayloadBid = "validateApiExecutionPayloadBid",
|
|
29
28
|
validateGossipPayloadAttestationMessage = "validateGossipPayloadAttestationMessage",
|
|
30
29
|
validateGossipProposerPreferences = "validateGossipProposerPreferences",
|
|
31
30
|
onForkChoiceFinalized = "onForkChoiceFinalized",
|
|
@@ -83,186 +83,6 @@ function isBidCompatibleWithHead(
|
|
|
83
83
|
return buildsOnParentPayload;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/**
|
|
87
|
-
* Validation for bids submitted via the API by the operator's own builder.
|
|
88
|
-
*
|
|
89
|
-
* Only non-transient checks are applied: slot later than parent, zero execution payment, blob
|
|
90
|
-
* commitment count, builder eligibility, version and balance coverage, prev_randao, and signature.
|
|
91
|
-
* Transient gossip rules (head compatibility, first bid per tuple, value increment, proposer
|
|
92
|
-
* preferences) are not applied, since those only limit forwarding of peers' messages and the
|
|
93
|
-
* builder may legitimately bid on a branch this node does not consider head.
|
|
94
|
-
* The parent-payload builder-exit rule is omitted because this endpoint only accepts bids from
|
|
95
|
-
* the operator's own builder.
|
|
96
|
-
*
|
|
97
|
-
* Throws on any failed check. Also throws if the bid's parent block is unknown or its state is
|
|
98
|
-
* unavailable, since the checks cannot be evaluated against the parent branch.
|
|
99
|
-
*/
|
|
100
|
-
export async function validateApiExecutionPayloadBid(
|
|
101
|
-
chain: IBeaconChain,
|
|
102
|
-
signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
|
|
103
|
-
): Promise<void> {
|
|
104
|
-
const bid = signedExecutionPayloadBid.message;
|
|
105
|
-
const bidParentBlockRoot = toRootHex(bid.parentBlockRoot);
|
|
106
|
-
|
|
107
|
-
// [REJECT] `bid.execution_payment` is zero.
|
|
108
|
-
if (bid.executionPayment !== 0n) {
|
|
109
|
-
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
110
|
-
code: ExecutionPayloadBidErrorCode.NON_ZERO_EXECUTION_PAYMENT,
|
|
111
|
-
builderIndex: bid.builderIndex,
|
|
112
|
-
executionPayment: bid.executionPayment,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// [REJECT] The bid's block hash is not equal to its parent block hash -- i.e.
|
|
117
|
-
// `bid.block_hash != bid.parent_block_hash`.
|
|
118
|
-
if (byteArrayEquals(bid.blockHash, bid.parentBlockHash)) {
|
|
119
|
-
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
120
|
-
code: ExecutionPayloadBidErrorCode.BLOCK_HASH_EQUALS_PARENT_BLOCK_HASH,
|
|
121
|
-
blockHash: toRootHex(bid.blockHash),
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
|
|
126
|
-
// consensus layer -- i.e. validate that
|
|
127
|
-
// `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
|
|
128
|
-
const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
|
|
129
|
-
const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
|
|
130
|
-
if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
|
|
131
|
-
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
132
|
-
code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS,
|
|
133
|
-
blobKzgCommitmentsLen,
|
|
134
|
-
commitmentLimit: maxBlobsPerBlock,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// [IGNORE] `bid.slot` is the current slot, or the next slot (`bid.slot - 1` is current), allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
|
|
139
|
-
if (
|
|
140
|
-
!chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot) &&
|
|
141
|
-
!chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot - 1)
|
|
142
|
-
) {
|
|
143
|
-
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
144
|
-
code: ExecutionPayloadBidErrorCode.INVALID_SLOT,
|
|
145
|
-
builderIndex: bid.builderIndex,
|
|
146
|
-
slot: bid.slot,
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// [IGNORE] The bid is received after the inclusion list deadline of `bid.slot - 1`
|
|
151
|
-
if (
|
|
152
|
-
isForkPostHeze(chain.config.getForkName(bid.slot)) &&
|
|
153
|
-
chain.clock.msFromSlot(bid.slot - 1) < chain.config.getInclusionListDueMs(chain.config.getForkName(bid.slot - 1))
|
|
154
|
-
) {
|
|
155
|
-
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
156
|
-
code: ExecutionPayloadBidErrorCode.BEFORE_INCLUSION_LIST_DEADLINE,
|
|
157
|
-
builderIndex: bid.builderIndex,
|
|
158
|
-
slot: bid.slot,
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
|
|
163
|
-
// Moved earlier than the spec ordering so we can derive the proposer dependent root for the
|
|
164
|
-
// proposer-preferences lookup below from a known fork-choice block.
|
|
165
|
-
const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(bidParentBlockRoot);
|
|
166
|
-
if (parentBlock === null) {
|
|
167
|
-
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
168
|
-
code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
|
|
169
|
-
parentBlockRoot: bidParentBlockRoot,
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// [REJECT] The bid is for a higher slot than its parent block -- i.e.
|
|
174
|
-
// validate that `bid.slot` is greater than the slot of the block with root
|
|
175
|
-
// `bid.parent_block_root`.
|
|
176
|
-
if (bid.slot <= parentBlock.slot) {
|
|
177
|
-
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
178
|
-
code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT,
|
|
179
|
-
parentSlot: parentBlock.slot,
|
|
180
|
-
slot: bid.slot,
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const state = await chain.regen
|
|
185
|
-
.getBlockSlotState(parentBlock, bid.slot, {dontTransferCache: true}, RegenCaller.validateApiExecutionPayloadBid)
|
|
186
|
-
.catch(() => {
|
|
187
|
-
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
188
|
-
code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
|
|
189
|
-
parentBlockRoot: bidParentBlockRoot,
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
if (!isStatePostGloas(state)) {
|
|
194
|
-
throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// [REJECT] `bid.builder_index` is within bounds -- i.e. `bid.builder_index < len(state.builders)`.
|
|
198
|
-
// `state.getBuilder` returns a lazy SSZ `getReadonly` view that is not bounds-checked eagerly; an
|
|
199
|
-
// out-of-range index only throws (`LeafNode has no right node`) on deferred field access (e.g. inside
|
|
200
|
-
// `isActiveBuilder`), escaping a try/catch around `getBuilder`. Check the length explicitly instead.
|
|
201
|
-
if (bid.builderIndex >= state.getBuildersLength()) {
|
|
202
|
-
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
203
|
-
code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
|
|
204
|
-
builderIndex: bid.builderIndex,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
|
|
209
|
-
// `is_active_builder(state, bid.builder_index)` returns `True`.
|
|
210
|
-
const builder = state.getBuilder(bid.builderIndex);
|
|
211
|
-
if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
|
|
212
|
-
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
213
|
-
code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
|
|
214
|
-
builderIndex: bid.builderIndex,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// [REJECT] The builder version is `PAYLOAD_BUILDER_VERSION` -- i.e.
|
|
219
|
-
// `state.builders[bid.builder_index].version == PAYLOAD_BUILDER_VERSION`.
|
|
220
|
-
if (builder.version !== PAYLOAD_BUILDER_VERSION) {
|
|
221
|
-
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
222
|
-
code: ExecutionPayloadBidErrorCode.INVALID_BUILDER_VERSION,
|
|
223
|
-
builderIndex: bid.builderIndex,
|
|
224
|
-
version: builder.version,
|
|
225
|
-
expectedVersion: PAYLOAD_BUILDER_VERSION,
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// [IGNORE] `bid.value` is less or equal than the builder's excess balance --
|
|
230
|
-
// i.e. `can_builder_cover_bid(state, builder_index, amount)` returns `True`.
|
|
231
|
-
if (!state.canBuilderCoverBid(bid.builderIndex, bid.value)) {
|
|
232
|
-
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
233
|
-
code: ExecutionPayloadBidErrorCode.BID_TOO_HIGH,
|
|
234
|
-
bidValue: bid.value,
|
|
235
|
-
builderBalance: builder.balance,
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// [REJECT] `bid.prev_randao` is the correct RANDAO mix -- i.e. validate that
|
|
240
|
-
// `bid.prev_randao == get_randao_mix(parent_state, get_current_epoch(parent_state))`.
|
|
241
|
-
const randaoMix = state.getRandaoMix(computeEpochAtSlot(state.slot));
|
|
242
|
-
if (!byteArrayEquals(bid.prevRandao, randaoMix)) {
|
|
243
|
-
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
244
|
-
code: ExecutionPayloadBidErrorCode.INVALID_PREV_RANDAO,
|
|
245
|
-
builderIndex: bid.builderIndex,
|
|
246
|
-
bidPrevRandao: toHex(bid.prevRandao),
|
|
247
|
-
expectedPrevRandao: toHex(randaoMix),
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
|
|
252
|
-
const signatureSet = createSingleSignatureSetFromComponents(
|
|
253
|
-
builder.pubkey,
|
|
254
|
-
getExecutionPayloadBidSigningRoot(chain.config, bid),
|
|
255
|
-
signedExecutionPayloadBid.signature
|
|
256
|
-
);
|
|
257
|
-
if (!(await chain.bls.verifySignatureSets([signatureSet]))) {
|
|
258
|
-
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
259
|
-
code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE,
|
|
260
|
-
builderIndex: bid.builderIndex,
|
|
261
|
-
slot: bid.slot,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
86
|
export async function validateGossipExecutionPayloadBid(
|
|
267
87
|
chain: IBeaconChain,
|
|
268
88
|
signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
|
package/src/db/beacon.ts
CHANGED
|
@@ -130,6 +130,11 @@ export class BeaconDb implements IBeaconDb {
|
|
|
130
130
|
await this.blobSidecars.batchDelete(await this.blobSidecars.keys());
|
|
131
131
|
// New columns are persisted in flat files, so this bucket only contains legacy hot data.
|
|
132
132
|
await this.deleteBucketData(Bucket.allForks_dataColumnSidecars);
|
|
133
|
+
// Compact even if empty to reclaim tombstones left by previous startups.
|
|
134
|
+
await this.db.compactRange(
|
|
135
|
+
encodeKey(Bucket.allForks_dataColumnSidecars, Buffer.alloc(0)),
|
|
136
|
+
encodeKey(Bucket.allForks_dataColumnSidecars + 1, Buffer.alloc(0))
|
|
137
|
+
);
|
|
133
138
|
// Prune all hot blocks
|
|
134
139
|
// TODO: Enable once it's deemed safe
|
|
135
140
|
// await this.block.batchDelete(await this.block.keys());
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,8 @@ export {persistAnchorState} from "./chain/index.js";
|
|
|
6
6
|
export {DbCPStateDatastore} from "./chain/stateCache/datastore/db.js";
|
|
7
7
|
export {FileCPStateDatastore} from "./chain/stateCache/datastore/file.js";
|
|
8
8
|
export {BeaconDb, type IBeaconDb} from "./db/index.js";
|
|
9
|
+
// Export execution engine to make it usable by builder
|
|
10
|
+
export {initializeExecutionEngine} from "./execution/index.js";
|
|
9
11
|
// Export metrics utilities to de-duplicate validator metrics
|
|
10
12
|
export {
|
|
11
13
|
type HttpMetricsServer,
|
|
@@ -1474,6 +1474,11 @@ export function createLodestarMetrics(
|
|
|
1474
1474
|
help: "Total number of InsertOutcome as a result of adding an execution payload bid from gossip to the pool",
|
|
1475
1475
|
labelNames: ["insertOutcome"],
|
|
1476
1476
|
}),
|
|
1477
|
+
apiInsertOutcome: register.counter<{insertOutcome: InsertOutcome}>({
|
|
1478
|
+
name: "lodestar_oppool_execution_payload_bid_pool_api_insert_outcome_total",
|
|
1479
|
+
help: "Total number of InsertOutcome as a result of adding an execution payload bid from api to the pool",
|
|
1480
|
+
labelNames: ["insertOutcome"],
|
|
1481
|
+
}),
|
|
1477
1482
|
apiValidationTime: register.histogram({
|
|
1478
1483
|
name: "lodestar_api_execution_payload_bid_validation_time_seconds",
|
|
1479
1484
|
help: "Time elapsed for signed execution payload bid validation - api path",
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
deneb,
|
|
25
25
|
fulu,
|
|
26
26
|
gloas,
|
|
27
|
+
isGloasBeaconBlock,
|
|
27
28
|
isGloasDataColumnSidecar,
|
|
28
29
|
ssz,
|
|
29
30
|
sszTypesFor,
|
|
@@ -150,6 +151,31 @@ export function getGossipHandlers(modules: ValidatorFnsModules, options: GossipH
|
|
|
150
151
|
*/
|
|
151
152
|
function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHandlerOpts): SequentialGossipHandlers {
|
|
152
153
|
const {chain, config, metrics, logger, core} = modules;
|
|
154
|
+
const incompletePayloadWaits = new WeakSet<PayloadEnvelopeInput>();
|
|
155
|
+
|
|
156
|
+
function scheduleIncompletePayloadEnvelope(payloadInput: PayloadEnvelopeInput, peer: string): void {
|
|
157
|
+
if (payloadInput.isComplete() || incompletePayloadWaits.has(payloadInput)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
incompletePayloadWaits.add(payloadInput);
|
|
161
|
+
|
|
162
|
+
const fork = config.getForkName(payloadInput.slot);
|
|
163
|
+
const cutoffTimeMs = getCutoffTimeMs(chain, payloadInput.slot, config.getPayloadDueMs(fork));
|
|
164
|
+
payloadInput.waitForEnvelopeAndAllData(cutoffTimeMs).catch(() => {
|
|
165
|
+
if (
|
|
166
|
+
payloadInput.isComplete() ||
|
|
167
|
+
chain.seenPayloadEnvelopeInputCache.get(payloadInput.blockRootHex) !== payloadInput
|
|
168
|
+
) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
chain.logger.debug("Payload envelope incomplete at payload deadline", payloadInput.getLogMeta());
|
|
172
|
+
chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, {
|
|
173
|
+
payloadInput,
|
|
174
|
+
peer,
|
|
175
|
+
source: BlockInputSource.gossip,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
153
179
|
|
|
154
180
|
/**
|
|
155
181
|
* Add a gossip block to the seen caches. The proposer signature of the block MUST be verified.
|
|
@@ -764,7 +790,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
764
790
|
// roots per proposer and slot, which bounds full imports over gossip to one alternate
|
|
765
791
|
chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root) &&
|
|
766
792
|
// A sibling with an unknown parent cannot be imported, sync fetches it if its branch becomes relevant
|
|
767
|
-
chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) !== null
|
|
793
|
+
chain.forkChoice.getBlockHexDefaultStatus(toRootHex(signedBlock.message.parentRoot)) !== null &&
|
|
794
|
+
(!isGloasBeaconBlock(signedBlock.message) ||
|
|
795
|
+
chain.forkChoice.getBlockHexAndBlockHash(
|
|
796
|
+
toRootHex(signedBlock.message.parentRoot),
|
|
797
|
+
toRootHex(signedBlock.message.body.signedExecutionPayloadBid.message.parentBlockHash)
|
|
798
|
+
) !== null)
|
|
768
799
|
) {
|
|
769
800
|
const blockInput = cacheSignatureVerifiedBlock(
|
|
770
801
|
signedBlock,
|
|
@@ -902,25 +933,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
902
933
|
// envelope arrival (gossip or API). An in-flight importExecutionPayload is awaiting
|
|
903
934
|
// payloadInput.waitForAllData(); addColumn above will resolve it once hasAllData flips.
|
|
904
935
|
|
|
905
|
-
|
|
906
|
-
const cutoffTimeMs = getCutoffTimeMs(chain, dataColumnSlot, BLOCK_AVAILABILITY_CUTOFF_MS);
|
|
907
|
-
// do not await here to not delay gossip validation
|
|
908
|
-
payloadInput.waitForEnvelopeAndAllData(cutoffTimeMs).catch((_e) => {
|
|
909
|
-
chain.logger.debug(
|
|
910
|
-
"Waited for envelope and data after receiving gossip column. Cut-off reached so emitting incompletePayloadEnvelope",
|
|
911
|
-
{
|
|
912
|
-
dataColumnIndex: index,
|
|
913
|
-
...payloadInputMeta,
|
|
914
|
-
}
|
|
915
|
-
);
|
|
916
|
-
// TODO GLOAS: UnknownBlockSync to handle this event
|
|
917
|
-
chain.emitter.emit(ChainEvent.incompletePayloadEnvelope, {
|
|
918
|
-
payloadInput,
|
|
919
|
-
peer: peerIdStr,
|
|
920
|
-
source: BlockInputSource.gossip,
|
|
921
|
-
});
|
|
922
|
-
});
|
|
923
|
-
}
|
|
936
|
+
scheduleIncompletePayloadEnvelope(payloadInput, peerIdStr);
|
|
924
937
|
} catch (e) {
|
|
925
938
|
logger.debug(
|
|
926
939
|
"Error handling gossip data column",
|
|
@@ -1330,6 +1343,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1330
1343
|
peerIdStr,
|
|
1331
1344
|
});
|
|
1332
1345
|
|
|
1346
|
+
scheduleIncompletePayloadEnvelope(payloadInput, peerIdStr);
|
|
1347
|
+
|
|
1333
1348
|
chain.emitter.emit(routes.events.EventType.executionPayloadGossip, {
|
|
1334
1349
|
slot,
|
|
1335
1350
|
builderIndex: envelope.builderIndex,
|
package/src/sync/unknownBlock.ts
CHANGED
|
@@ -300,10 +300,19 @@ export class BlockInputSync {
|
|
|
300
300
|
|
|
301
301
|
private onIncompletePayloadEnvelope = (data: ChainEventData[ChainEvent.incompletePayloadEnvelope]): void => {
|
|
302
302
|
try {
|
|
303
|
-
this.addByPayloadInput(data.payloadInput, data.peer);
|
|
304
|
-
this.
|
|
305
|
-
|
|
306
|
-
|
|
303
|
+
const isNewRoot = this.addByPayloadInput(data.payloadInput, data.peer);
|
|
304
|
+
const pendingPayload = this.pendingPayloads.get(data.payloadInput.blockRootHex);
|
|
305
|
+
if (
|
|
306
|
+
pendingPayload &&
|
|
307
|
+
pendingPayload.status !== PendingPayloadInputStatus.fetching &&
|
|
308
|
+
pendingPayload.status !== PendingPayloadInputStatus.processing
|
|
309
|
+
) {
|
|
310
|
+
this.triggerUnknownBlockSearch();
|
|
311
|
+
}
|
|
312
|
+
if (isNewRoot) {
|
|
313
|
+
this.metrics?.blockInputSync.requests.inc({type: PendingBlockType.INCOMPLETE_PAYLOAD_ENVELOPE});
|
|
314
|
+
this.metrics?.blockInputSync.payloadSource.inc({source: data.source});
|
|
315
|
+
}
|
|
307
316
|
} catch (e) {
|
|
308
317
|
this.logger.debug("Error handling incompletePayloadEnvelope event", {}, e as Error);
|
|
309
318
|
}
|
|
@@ -599,43 +608,44 @@ export class BlockInputSync {
|
|
|
599
608
|
return this.chain.seenPayloadEnvelopeInputCache.get(rootHex)?.slot;
|
|
600
609
|
}
|
|
601
610
|
|
|
602
|
-
private addByPayloadInput = (
|
|
603
|
-
payloadInput: PayloadEnvelopeInput,
|
|
604
|
-
peerIdStr?: PeerIdStr,
|
|
605
|
-
envelope?: gloas.SignedExecutionPayloadEnvelope
|
|
606
|
-
): void => {
|
|
611
|
+
private addByPayloadInput = (payloadInput: PayloadEnvelopeInput, peerIdStr?: PeerIdStr): boolean => {
|
|
607
612
|
if (this.knownBadBlocks.has(payloadInput.blockRootHex)) {
|
|
608
613
|
this.logger.debug("Ignoring payload input for known bad block root", {
|
|
609
614
|
slot: payloadInput.slot,
|
|
610
615
|
root: payloadInput.blockRootHex,
|
|
611
616
|
peerIdStr,
|
|
612
617
|
});
|
|
613
|
-
return;
|
|
618
|
+
return false;
|
|
614
619
|
}
|
|
615
620
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
+
let pendingPayload = this.pendingPayloads.get(payloadInput.blockRootHex);
|
|
622
|
+
const isNewRoot = pendingPayload === undefined;
|
|
623
|
+
// Envelope-only entries must go through reconciliation to validate their speculative envelope.
|
|
624
|
+
if (
|
|
625
|
+
!pendingPayload ||
|
|
626
|
+
pendingPayload.status === PendingPayloadInputStatus.pending ||
|
|
627
|
+
pendingPayload.status === PendingPayloadInputStatus.downloaded
|
|
628
|
+
) {
|
|
629
|
+
pendingPayload = this.toPendingPayloadInput(payloadInput, pendingPayload);
|
|
630
|
+
this.pendingPayloads.set(payloadInput.blockRootHex, pendingPayload);
|
|
631
|
+
|
|
632
|
+
this.logger.verbose("Added payloadInput to BlockInputSync.pendingPayloads", {
|
|
633
|
+
slot: payloadInput.slot,
|
|
634
|
+
root: payloadInput.blockRootHex,
|
|
635
|
+
delaySec: this.chain.clock.secFromSlot(payloadInput.slot),
|
|
636
|
+
});
|
|
637
|
+
}
|
|
621
638
|
|
|
622
639
|
if (peerIdStr && pendingPayload.peerIdStrings.size < MAX_PEERS_PER_ROOT) {
|
|
623
640
|
pendingPayload.peerIdStrings.add(peerIdStr);
|
|
624
641
|
}
|
|
625
642
|
|
|
626
|
-
this.pendingPayloads.set(payloadInput.blockRootHex, pendingPayload);
|
|
627
|
-
|
|
628
|
-
this.logger.verbose("Added payloadInput to BlockInputSync.pendingPayloads", {
|
|
629
|
-
slot: payloadInput.slot,
|
|
630
|
-
root: payloadInput.blockRootHex,
|
|
631
|
-
delaySec: this.chain.clock.secFromSlot(payloadInput.slot),
|
|
632
|
-
});
|
|
633
|
-
|
|
634
643
|
const prunedItemCount = pruneSetToMax(this.pendingPayloads, this.maxPendingBlocks);
|
|
635
644
|
if (prunedItemCount > 0) {
|
|
636
645
|
this.metrics?.blockInputSync.removedPayloads.inc({reason: DroppedItemReason.capacity}, prunedItemCount);
|
|
637
646
|
this.logger.verbose(`Pruned ${prunedItemCount} items from BlockInputSync.pendingPayloads`);
|
|
638
647
|
}
|
|
648
|
+
return isNewRoot;
|
|
639
649
|
};
|
|
640
650
|
|
|
641
651
|
private onPeerConnected = (data: NetworkEventData[NetworkEvent.peerConnected]): void => {
|
|
@@ -1267,6 +1277,9 @@ export class BlockInputSync {
|
|
|
1267
1277
|
}
|
|
1268
1278
|
|
|
1269
1279
|
const payloadInput = await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
|
|
1280
|
+
if (this.pendingPayloads.get(rootHex) !== pendingPayload) {
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1270
1283
|
if (!payloadInput) {
|
|
1271
1284
|
this.logger.debug("PayloadEnvelopeInput not yet reloadable for imported block, will retry", {
|
|
1272
1285
|
root: rootHex,
|
|
@@ -1278,6 +1291,9 @@ export class BlockInputSync {
|
|
|
1278
1291
|
const validationResult = await wrapError(
|
|
1279
1292
|
validateGossipExecutionPayloadEnvelope(this.chain, pendingPayload.envelope)
|
|
1280
1293
|
);
|
|
1294
|
+
if (this.pendingPayloads.get(rootHex) !== pendingPayload) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1281
1297
|
if (validationResult.err) {
|
|
1282
1298
|
this.logger.debug(
|
|
1283
1299
|
"Pending payload envelope failed validation after block import, refetching by root",
|
|
@@ -1345,14 +1361,13 @@ export class BlockInputSync {
|
|
|
1345
1361
|
payload.status = PendingPayloadInputStatus.fetching;
|
|
1346
1362
|
|
|
1347
1363
|
const res = await wrapError(this.fetchPayloadInput(payload));
|
|
1364
|
+
if (this.pendingPayloads.get(rootHex) !== payload) {
|
|
1365
|
+
this.logger.verbose("Dropping downloaded payload, entry changed during fetch", logCtx);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1348
1368
|
if (!res.err) {
|
|
1349
1369
|
const pendingPayload = res.result;
|
|
1350
1370
|
const resultRootHex = getPayloadSyncCacheItemRootHex(pendingPayload);
|
|
1351
|
-
// finalization may have deleted this entry while we were awaiting the network, do not resurrect it via the set below
|
|
1352
|
-
if (!this.pendingPayloads.has(resultRootHex)) {
|
|
1353
|
-
this.logger.verbose("Dropping downloaded payload, entry pruned during fetch", logCtx);
|
|
1354
|
-
return;
|
|
1355
|
-
}
|
|
1356
1371
|
this.pendingPayloads.set(resultRootHex, pendingPayload);
|
|
1357
1372
|
const result = isPendingPayloadEnvelope(pendingPayload)
|
|
1358
1373
|
? DownloadResult.WaitingForBlock
|
|
@@ -1423,12 +1438,16 @@ export class BlockInputSync {
|
|
|
1423
1438
|
this.logger.debug("Processing downloaded payload", logCtx);
|
|
1424
1439
|
|
|
1425
1440
|
const res = await wrapError(this.chain.processExecutionPayload(pendingPayload.payloadInput));
|
|
1441
|
+
// The executionPayload event emitted during import usually removes the entry before this point
|
|
1426
1442
|
if (!res.err) {
|
|
1427
1443
|
this.logger.debug("Processed payload from unknown sync", logCtx);
|
|
1428
1444
|
this.pendingPayloads.delete(rootHex);
|
|
1429
1445
|
this.triggerUnknownBlockSearch();
|
|
1430
1446
|
return;
|
|
1431
1447
|
}
|
|
1448
|
+
if (this.pendingPayloads.get(rootHex) !== pendingPayload) {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1432
1451
|
|
|
1433
1452
|
if (res.err instanceof PayloadError) {
|
|
1434
1453
|
switch (res.err.type.code) {
|