@lodestar/beacon-node 1.47.0-rc.0 → 1.48.0-dev.7d85330f92
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 +4 -0
- package/lib/api/impl/beacon/blocks/index.js.map +1 -1
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +5 -0
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/blocks/index.js +7 -7
- package/lib/chain/blocks/index.js.map +1 -1
- package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
- package/lib/chain/blocks/verifyBlock.js +5 -2
- package/lib/chain/blocks/verifyBlock.js.map +1 -1
- package/lib/chain/chain.d.ts +1 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +2 -1
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/errors/executionPayloadBid.d.ts +5 -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/interface.d.ts +2 -2
- package/lib/chain/interface.d.ts.map +1 -1
- package/lib/chain/prepareNextSlot.d.ts +5 -0
- package/lib/chain/prepareNextSlot.d.ts.map +1 -1
- package/lib/chain/prepareNextSlot.js +24 -1
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/produceBlock/computeNewStateRoot.d.ts.map +1 -1
- package/lib/chain/produceBlock/computeNewStateRoot.js +3 -25
- package/lib/chain/produceBlock/computeNewStateRoot.js.map +1 -1
- package/lib/chain/regen/interface.d.ts +1 -0
- package/lib/chain/regen/interface.d.ts.map +1 -1
- package/lib/chain/regen/interface.js +1 -0
- package/lib/chain/regen/interface.js.map +1 -1
- package/lib/chain/validation/block.d.ts.map +1 -1
- package/lib/chain/validation/block.js +40 -10
- package/lib/chain/validation/block.js.map +1 -1
- package/lib/chain/validation/executionPayloadBid.d.ts +15 -3
- package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/validation/executionPayloadBid.js +163 -8
- package/lib/chain/validation/executionPayloadBid.js.map +1 -1
- package/lib/execution/builder/apiClient.d.ts +6 -0
- package/lib/execution/builder/apiClient.d.ts.map +1 -1
- package/lib/execution/builder/apiClient.js +19 -1
- package/lib/execution/builder/apiClient.js.map +1 -1
- package/lib/execution/builder/validateBid.d.ts +1 -0
- package/lib/execution/builder/validateBid.d.ts.map +1 -1
- package/lib/execution/builder/validateBid.js +9 -1
- package/lib/execution/builder/validateBid.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +10 -1
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +19 -3
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/network.d.ts.map +1 -1
- package/lib/network/network.js +5 -4
- package/lib/network/network.js.map +1 -1
- package/lib/network/processor/gossipQueues/indexed.d.ts.map +1 -1
- package/lib/network/processor/gossipQueues/indexed.js +0 -1
- package/lib/network/processor/gossipQueues/indexed.js.map +1 -1
- package/lib/network/processor/index.d.ts.map +1 -1
- package/lib/network/processor/index.js +16 -8
- package/lib/network/processor/index.js.map +1 -1
- package/lib/sync/range/chain.d.ts.map +1 -1
- package/lib/sync/range/chain.js +9 -2
- package/lib/sync/range/chain.js.map +1 -1
- package/lib/sync/range/range.js +1 -1
- package/lib/sync/range/range.js.map +1 -1
- package/package.json +15 -15
- package/src/api/impl/beacon/blocks/index.ts +4 -0
- package/src/api/impl/validator/index.ts +5 -0
- package/src/chain/blocks/index.ts +7 -7
- package/src/chain/blocks/verifyBlock.ts +5 -2
- package/src/chain/chain.ts +2 -1
- package/src/chain/errors/executionPayloadBid.ts +6 -0
- package/src/chain/interface.ts +2 -2
- package/src/chain/prepareNextSlot.ts +26 -1
- package/src/chain/produceBlock/computeNewStateRoot.ts +3 -38
- package/src/chain/regen/interface.ts +1 -0
- package/src/chain/validation/block.ts +43 -10
- package/src/chain/validation/executionPayloadBid.ts +190 -10
- package/src/execution/builder/apiClient.ts +21 -1
- package/src/execution/builder/validateBid.ts +16 -1
- package/src/metrics/metrics/lodestar.ts +21 -4
- package/src/network/network.ts +7 -4
- package/src/network/processor/gossipQueues/indexed.ts +0 -1
- package/src/network/processor/index.ts +25 -11
- package/src/sync/range/chain.ts +9 -2
- package/src/sync/range/range.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {ProtoBlock} from "@lodestar/fork-choice";
|
|
2
2
|
import {PAYLOAD_BUILDER_VERSION} from "@lodestar/params";
|
|
3
3
|
import {
|
|
4
4
|
computeEpochAtSlot,
|
|
@@ -46,11 +46,12 @@ function getMinBidValue(currentHighestBid: number): number {
|
|
|
46
46
|
/**
|
|
47
47
|
* Check whether a bid builds on one of the paths compatible with the local head branch.
|
|
48
48
|
*
|
|
49
|
-
* Building directly on the parent is allowed for proposer-boost reorgs
|
|
49
|
+
* Building directly on the parent is allowed for proposer-boost reorgs, including at epoch
|
|
50
|
+
* boundaries when the head is weak enough that a reorg onto the strong parent is predicted.
|
|
50
51
|
* Otherwise the bid must build on the local head's full or empty payload variant, as selected for its slot.
|
|
51
52
|
*/
|
|
52
53
|
function isBidCompatibleWithHead(
|
|
53
|
-
|
|
54
|
+
chain: IBeaconChain,
|
|
54
55
|
head: ProtoBlock,
|
|
55
56
|
bidSlot: Slot,
|
|
56
57
|
bidParentBlockRoot: RootHex,
|
|
@@ -60,9 +61,13 @@ function isBidCompatibleWithHead(
|
|
|
60
61
|
const buildsOnParentPayload = bidParentBlockHash === head.parentBlockHash;
|
|
61
62
|
|
|
62
63
|
if (buildsOnParentBlock && buildsOnParentPayload) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
if (!isStartSlotOfEpoch(bidSlot)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
// Epoch-boundary reorgs are allowed, but only propagate the bid if its parent matches the block
|
|
68
|
+
// we predict a proposer would reorg onto — predictProposerHead returns the strong parent only
|
|
69
|
+
// when the head is weak.
|
|
70
|
+
return chain.predictProposerHead().blockRoot === bidParentBlockRoot;
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
if (bidParentBlockRoot !== head.blockRoot) {
|
|
@@ -71,18 +76,170 @@ function isBidCompatibleWithHead(
|
|
|
71
76
|
|
|
72
77
|
const buildsOnHeadPayload = bidParentBlockHash === head.executionPayloadBlockHash;
|
|
73
78
|
|
|
74
|
-
if (forkChoice.shouldBuildOnFull(head, bidSlot)) {
|
|
79
|
+
if (chain.forkChoice.shouldBuildOnFull(head, bidSlot)) {
|
|
75
80
|
return buildsOnHeadPayload;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
return buildsOnParentPayload;
|
|
79
84
|
}
|
|
80
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
|
+
*/
|
|
81
100
|
export async function validateApiExecutionPayloadBid(
|
|
82
101
|
chain: IBeaconChain,
|
|
83
102
|
signedExecutionPayloadBid: gloas.SignedExecutionPayloadBid
|
|
84
|
-
): Promise<
|
|
85
|
-
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
const bid = signedExecutionPayloadBid.message;
|
|
105
|
+
const bidParentBlockRoot = toRootHex(bid.parentBlockRoot);
|
|
106
|
+
|
|
107
|
+
// [IGNORE] `bid.slot` is the current slot, or the next slot (`bid.slot - 1` is current), allowing for `MAXIMUM_GOSSIP_CLOCK_DISPARITY`.
|
|
108
|
+
if (
|
|
109
|
+
!chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot) &&
|
|
110
|
+
!chain.clock.isCurrentSlotGivenGossipDisparity(bid.slot - 1)
|
|
111
|
+
) {
|
|
112
|
+
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
113
|
+
code: ExecutionPayloadBidErrorCode.INVALID_SLOT,
|
|
114
|
+
builderIndex: bid.builderIndex,
|
|
115
|
+
slot: bid.slot,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
|
|
120
|
+
// Moved earlier than the spec ordering so we can derive the proposer dependent root for the
|
|
121
|
+
// proposer-preferences lookup below from a known fork-choice block.
|
|
122
|
+
const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(bidParentBlockRoot);
|
|
123
|
+
if (parentBlock === null) {
|
|
124
|
+
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
125
|
+
code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
|
|
126
|
+
parentBlockRoot: bidParentBlockRoot,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// [REJECT] The bid is for a higher slot than its parent block -- i.e.
|
|
131
|
+
// validate that `bid.slot` is greater than the slot of the block with root
|
|
132
|
+
// `bid.parent_block_root`.
|
|
133
|
+
if (bid.slot <= parentBlock.slot) {
|
|
134
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
135
|
+
code: ExecutionPayloadBidErrorCode.NOT_LATER_THAN_PARENT,
|
|
136
|
+
parentSlot: parentBlock.slot,
|
|
137
|
+
slot: bid.slot,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// [REJECT] `bid.execution_payment` is zero.
|
|
142
|
+
if (bid.executionPayment !== 0n) {
|
|
143
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
144
|
+
code: ExecutionPayloadBidErrorCode.NON_ZERO_EXECUTION_PAYMENT,
|
|
145
|
+
builderIndex: bid.builderIndex,
|
|
146
|
+
executionPayment: bid.executionPayment,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
|
|
151
|
+
// consensus layer -- i.e. validate that
|
|
152
|
+
// `len(bid.blob_kzg_commitments) <= get_blob_parameters(compute_epoch_at_slot(bid.slot)).max_blobs_per_block`.
|
|
153
|
+
const blobKzgCommitmentsLen = bid.blobKzgCommitments.length;
|
|
154
|
+
const maxBlobsPerBlock = chain.config.getMaxBlobsPerBlock(computeEpochAtSlot(bid.slot));
|
|
155
|
+
if (blobKzgCommitmentsLen > maxBlobsPerBlock) {
|
|
156
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
157
|
+
code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS,
|
|
158
|
+
blobKzgCommitmentsLen,
|
|
159
|
+
commitmentLimit: maxBlobsPerBlock,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const state = await chain.regen
|
|
164
|
+
.getBlockSlotState(parentBlock, bid.slot, {dontTransferCache: true}, RegenCaller.validateApiExecutionPayloadBid)
|
|
165
|
+
.catch(() => {
|
|
166
|
+
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
167
|
+
code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
|
|
168
|
+
parentBlockRoot: bidParentBlockRoot,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
if (!isStatePostGloas(state)) {
|
|
173
|
+
throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// [REJECT] `bid.builder_index` is within bounds -- i.e. `bid.builder_index < len(state.builders)`.
|
|
177
|
+
// `state.getBuilder` returns a lazy SSZ `getReadonly` view that is not bounds-checked eagerly; an
|
|
178
|
+
// out-of-range index only throws (`LeafNode has no right node`) on deferred field access (e.g. inside
|
|
179
|
+
// `isActiveBuilder`), escaping a try/catch around `getBuilder`. Check the length explicitly instead.
|
|
180
|
+
if (bid.builderIndex >= state.getBuildersLength()) {
|
|
181
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
182
|
+
code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
|
|
183
|
+
builderIndex: bid.builderIndex,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
|
|
188
|
+
// `is_active_builder(state, bid.builder_index)` returns `True`.
|
|
189
|
+
const builder = state.getBuilder(bid.builderIndex);
|
|
190
|
+
if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
|
|
191
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
192
|
+
code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
|
|
193
|
+
builderIndex: bid.builderIndex,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// [REJECT] The builder version is `PAYLOAD_BUILDER_VERSION` -- i.e.
|
|
198
|
+
// `state.builders[bid.builder_index].version == PAYLOAD_BUILDER_VERSION`.
|
|
199
|
+
if (builder.version !== PAYLOAD_BUILDER_VERSION) {
|
|
200
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
201
|
+
code: ExecutionPayloadBidErrorCode.INVALID_BUILDER_VERSION,
|
|
202
|
+
builderIndex: bid.builderIndex,
|
|
203
|
+
version: builder.version,
|
|
204
|
+
expectedVersion: PAYLOAD_BUILDER_VERSION,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// [IGNORE] `bid.value` is less or equal than the builder's excess balance --
|
|
209
|
+
// i.e. `can_builder_cover_bid(state, builder_index, amount)` returns `True`.
|
|
210
|
+
if (!state.canBuilderCoverBid(bid.builderIndex, bid.value)) {
|
|
211
|
+
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
212
|
+
code: ExecutionPayloadBidErrorCode.BID_TOO_HIGH,
|
|
213
|
+
bidValue: bid.value,
|
|
214
|
+
builderBalance: builder.balance,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// [REJECT] `bid.prev_randao` is the correct RANDAO mix -- i.e. validate that
|
|
219
|
+
// `bid.prev_randao == get_randao_mix(parent_state, get_current_epoch(parent_state))`.
|
|
220
|
+
const randaoMix = state.getRandaoMix(computeEpochAtSlot(state.slot));
|
|
221
|
+
if (!byteArrayEquals(bid.prevRandao, randaoMix)) {
|
|
222
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
223
|
+
code: ExecutionPayloadBidErrorCode.INVALID_PREV_RANDAO,
|
|
224
|
+
builderIndex: bid.builderIndex,
|
|
225
|
+
bidPrevRandao: toHex(bid.prevRandao),
|
|
226
|
+
expectedPrevRandao: toHex(randaoMix),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
|
|
231
|
+
const signatureSet = createSingleSignatureSetFromComponents(
|
|
232
|
+
builder.pubkey,
|
|
233
|
+
getExecutionPayloadBidSigningRoot(chain.config, bid),
|
|
234
|
+
signedExecutionPayloadBid.signature
|
|
235
|
+
);
|
|
236
|
+
if (!(await chain.bls.verifySignatureSets([signatureSet]))) {
|
|
237
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
238
|
+
code: ExecutionPayloadBidErrorCode.INVALID_SIGNATURE,
|
|
239
|
+
builderIndex: bid.builderIndex,
|
|
240
|
+
slot: bid.slot,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
86
243
|
}
|
|
87
244
|
|
|
88
245
|
export async function validateGossipExecutionPayloadBid(
|
|
@@ -114,7 +271,7 @@ async function validateExecutionPayloadBid(
|
|
|
114
271
|
|
|
115
272
|
// [IGNORE] The bid is compatible with the current head branch.
|
|
116
273
|
const head = chain.forkChoice.getHead();
|
|
117
|
-
if (!isBidCompatibleWithHead(chain
|
|
274
|
+
if (!isBidCompatibleWithHead(chain, head, bid.slot, bidParentBlockRoot, bidParentBlockHash)) {
|
|
118
275
|
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
119
276
|
code: ExecutionPayloadBidErrorCode.INCOMPATIBLE_WITH_HEAD,
|
|
120
277
|
slot: bid.slot,
|
|
@@ -342,6 +499,29 @@ async function validateExecutionPayloadBid(
|
|
|
342
499
|
});
|
|
343
500
|
}
|
|
344
501
|
|
|
502
|
+
// [IGNORE] The parent's payload does not try to exit the builder
|
|
503
|
+
if (byteArrayEquals(bid.parentBlockHash, state.latestExecutionPayloadBid.blockHash)) {
|
|
504
|
+
const requests = await chain.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot).catch(() => {
|
|
505
|
+
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
506
|
+
code: ExecutionPayloadBidErrorCode.UNKNOWN_PARENT_BLOCK_HASH,
|
|
507
|
+
parentBlockHash: bidParentBlockHash,
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
if (
|
|
511
|
+
requests.builderExits.some(
|
|
512
|
+
(request) =>
|
|
513
|
+
byteArrayEquals(request.pubkey, builder.pubkey) &&
|
|
514
|
+
byteArrayEquals(request.sourceAddress, builder.executionAddress)
|
|
515
|
+
)
|
|
516
|
+
) {
|
|
517
|
+
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
518
|
+
code: ExecutionPayloadBidErrorCode.BUILDER_MAY_EXIT,
|
|
519
|
+
builderIndex: bid.builderIndex,
|
|
520
|
+
parentBlockRoot: bidParentBlockRoot,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
345
525
|
// [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
|
|
346
526
|
const signatureSet = createSingleSignatureSetFromComponents(
|
|
347
527
|
builder.pubkey,
|
|
@@ -201,6 +201,26 @@ export class BuilderApiClient {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Establish the connection to every known builder outside the bid deadline, a cold TCP/TLS handshake
|
|
206
|
+
* to a distant builder can exhaust `BUILDER_BID_DEADLINE_MS` on its own, and a long idle connection
|
|
207
|
+
* may be half-open and only fail once written to.
|
|
208
|
+
*/
|
|
209
|
+
async checkStatus(): Promise<void> {
|
|
210
|
+
await Promise.all(
|
|
211
|
+
Array.from(this.clients.entries()).map(async ([url, client]) => {
|
|
212
|
+
try {
|
|
213
|
+
(await client.status()).assertOk();
|
|
214
|
+
this.metrics?.builderApi.statusChecks.inc({status: "success"});
|
|
215
|
+
this.logger?.debug("Builder is ready", {builder: toPrintableUrl(url)});
|
|
216
|
+
} catch (e) {
|
|
217
|
+
this.metrics?.builderApi.statusChecks.inc({status: "error"});
|
|
218
|
+
this.logger?.warn("Builder status check failed", {builder: toPrintableUrl(url)}, e as Error);
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
204
224
|
private async getOrCreateClient(
|
|
205
225
|
url: BuilderUrl,
|
|
206
226
|
proposerPubkey: BLSPubkey,
|
|
@@ -237,7 +257,7 @@ export class BuilderApiClient {
|
|
|
237
257
|
{config: this.config, metrics: this.metrics?.builderHttpClient, logger: this.logger}
|
|
238
258
|
);
|
|
239
259
|
this.clients.set(url, client);
|
|
240
|
-
this.logger?.
|
|
260
|
+
this.logger?.debug("External builder registered", {url: toPrintableUrl(url)});
|
|
241
261
|
}
|
|
242
262
|
return client;
|
|
243
263
|
}
|
|
@@ -37,10 +37,11 @@ export async function validateBuilderApiExecutionPayloadBid(
|
|
|
37
37
|
parentBlockHash: RootHex;
|
|
38
38
|
parentBlockRoot: RootHex;
|
|
39
39
|
entry: routes.validator.BuilderEntry;
|
|
40
|
+
getParentExecutionRequests: () => Promise<gloas.ExecutionRequests>;
|
|
40
41
|
}
|
|
41
42
|
): Promise<void> {
|
|
42
43
|
const bid = signedExecutionPayloadBid.message;
|
|
43
|
-
const {slot, parentBlock, parentBlockHash, parentBlockRoot, entry} = request;
|
|
44
|
+
const {slot, parentBlock, parentBlockHash, parentBlockRoot, entry, getParentExecutionRequests} = request;
|
|
44
45
|
|
|
45
46
|
if (bid.slot !== slot) {
|
|
46
47
|
throw Error(`Bid slot=${bid.slot} does not match requested slot=${slot}`);
|
|
@@ -118,6 +119,20 @@ export async function validateBuilderApiExecutionPayloadBid(
|
|
|
118
119
|
throw Error(`Invalid bid prevRandao=${toHex(bid.prevRandao)} expected=${toHex(randaoMix)}`);
|
|
119
120
|
}
|
|
120
121
|
|
|
122
|
+
// The parent's payload does not try to exit the builder
|
|
123
|
+
if (byteArrayEquals(bid.parentBlockHash, state.latestExecutionPayloadBid.blockHash)) {
|
|
124
|
+
const requests = await getParentExecutionRequests();
|
|
125
|
+
if (
|
|
126
|
+
requests.builderExits.some(
|
|
127
|
+
(request) =>
|
|
128
|
+
byteArrayEquals(request.pubkey, builder.pubkey) &&
|
|
129
|
+
byteArrayEquals(request.sourceAddress, builder.executionAddress)
|
|
130
|
+
)
|
|
131
|
+
) {
|
|
132
|
+
throw Error(`Bid builderIndex=${bid.builderIndex} may exit in the parent payload`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
// The builder must honor the proposer preferences it learned over gossip
|
|
122
137
|
const bidEpoch = computeEpochAtSlot(bid.slot);
|
|
123
138
|
const dependentRootHex = (() => {
|
|
@@ -83,15 +83,15 @@ export function createLodestarMetrics(
|
|
|
83
83
|
}),
|
|
84
84
|
jobTime: register.histogram<{topic: GossipType}>({
|
|
85
85
|
name: "lodestar_gossip_validation_queue_job_time_seconds",
|
|
86
|
-
help: "Time to process gossip validation queue job in seconds",
|
|
86
|
+
help: "Time to process gossip validation queue job in seconds (accepted messages only)",
|
|
87
87
|
labelNames: ["topic"],
|
|
88
|
-
buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2
|
|
88
|
+
buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2],
|
|
89
89
|
}),
|
|
90
90
|
jobWaitTime: register.histogram<{topic: GossipType}>({
|
|
91
91
|
name: "lodestar_gossip_validation_queue_job_wait_time_seconds",
|
|
92
92
|
help: "Time from job added to the queue to starting the job in seconds",
|
|
93
93
|
labelNames: ["topic"],
|
|
94
|
-
buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2
|
|
94
|
+
buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 2],
|
|
95
95
|
}),
|
|
96
96
|
concurrency: register.gauge<{topic: GossipType}>({
|
|
97
97
|
name: "lodestar_gossip_validation_queue_concurrency",
|
|
@@ -104,9 +104,10 @@ export function createLodestarMetrics(
|
|
|
104
104
|
help: "Age of the first item of each key in the indexed queues in seconds",
|
|
105
105
|
buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 5],
|
|
106
106
|
}),
|
|
107
|
-
queueTime: register.histogram({
|
|
107
|
+
queueTime: register.histogram<{topic: GossipType}>({
|
|
108
108
|
name: "lodestar_gossip_validation_queue_time_seconds",
|
|
109
109
|
help: "Total time an item stays in queue until it is processed in seconds",
|
|
110
|
+
labelNames: ["topic"],
|
|
110
111
|
buckets: [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1, 5],
|
|
111
112
|
}),
|
|
112
113
|
},
|
|
@@ -920,6 +921,12 @@ export function createLodestarMetrics(
|
|
|
920
921
|
help: "Count of errors, by error type, while processing blocks",
|
|
921
922
|
labelNames: ["error"],
|
|
922
923
|
}),
|
|
924
|
+
|
|
925
|
+
preStateSource: register.counter<{source: "parentState" | "fallbackPreState" | "preState"}>({
|
|
926
|
+
name: "lodestar_gossip_block_validation_pre_state_source_total",
|
|
927
|
+
help: "Source of the pre-state used for gossip block validation",
|
|
928
|
+
labelNames: ["source"],
|
|
929
|
+
}),
|
|
923
930
|
},
|
|
924
931
|
gossipBlob: {
|
|
925
932
|
recvToValidation: register.histogram({
|
|
@@ -1350,6 +1357,11 @@ export function createLodestarMetrics(
|
|
|
1350
1357
|
help: "Total number of InsertOutcome as a result of adding an execution payload bid from api to the pool",
|
|
1351
1358
|
labelNames: ["insertOutcome"],
|
|
1352
1359
|
}),
|
|
1360
|
+
apiValidationTime: register.histogram({
|
|
1361
|
+
name: "lodestar_api_execution_payload_bid_validation_time_seconds",
|
|
1362
|
+
help: "Time elapsed for signed execution payload bid validation - api path",
|
|
1363
|
+
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.5],
|
|
1364
|
+
}),
|
|
1353
1365
|
},
|
|
1354
1366
|
},
|
|
1355
1367
|
|
|
@@ -2030,6 +2042,11 @@ export function createLodestarMetrics(
|
|
|
2030
2042
|
},
|
|
2031
2043
|
|
|
2032
2044
|
builderApi: {
|
|
2045
|
+
statusChecks: register.counter<{status: "success" | "error"}>({
|
|
2046
|
+
name: "lodestar_builder_api_status_checks_total",
|
|
2047
|
+
help: "Total count of status checks sent to external builders ahead of a proposal",
|
|
2048
|
+
labelNames: ["status"],
|
|
2049
|
+
}),
|
|
2033
2050
|
bidRequests: register.counter({
|
|
2034
2051
|
name: "lodestar_builder_api_bid_requests_total",
|
|
2035
2052
|
help: "Total count of execution payload bid requests sent to external builders",
|
package/src/network/network.ts
CHANGED
|
@@ -516,7 +516,8 @@ export class Network implements INetwork {
|
|
|
516
516
|
|
|
517
517
|
return this.publishGossip<GossipType.execution_payload_bid>(
|
|
518
518
|
{type: GossipType.execution_payload_bid, boundary},
|
|
519
|
-
signedBid
|
|
519
|
+
signedBid,
|
|
520
|
+
{floodPublish: true}
|
|
520
521
|
);
|
|
521
522
|
}
|
|
522
523
|
|
|
@@ -542,17 +543,19 @@ export class Network implements INetwork {
|
|
|
542
543
|
|
|
543
544
|
private async publishGossip<K extends GossipType>(
|
|
544
545
|
topic: GossipTopicMap[K],
|
|
545
|
-
object: GossipTypeMap[K]
|
|
546
|
+
object: GossipTypeMap[K],
|
|
547
|
+
opts?: PublishOpts
|
|
546
548
|
): Promise<number> {
|
|
547
549
|
const topicStr = stringifyGossipTopic(this.config, topic);
|
|
548
550
|
const sszType = getGossipSSZType(topic);
|
|
549
551
|
const messageData = (sszType.serialize as (object: GossipTypeMap[GossipType]) => Uint8Array)(object);
|
|
550
|
-
const
|
|
552
|
+
const publishOpts: PublishOpts = {
|
|
551
553
|
ignoreDuplicatePublishError: gossipTopicIgnoreDuplicatePublishError[topic.type],
|
|
552
554
|
// Leave undefined unless the topic opts out, so `--network.allowPublishToZeroPeers` still applies
|
|
553
555
|
allowPublishToZeroTopicPeers: gossipTopicAllowPublishToZeroPeers[topic.type] ? true : undefined,
|
|
556
|
+
...opts,
|
|
554
557
|
};
|
|
555
|
-
const sentPeers = await this.core.publishGossip(topicStr, messageData,
|
|
558
|
+
const sentPeers = await this.core.publishGossip(topicStr, messageData, publishOpts);
|
|
556
559
|
|
|
557
560
|
this.logger.verbose("Publish to topic", {topic: topicStr, sentPeers, currentSlot: this.clock.currentSlot});
|
|
558
561
|
return sentPeers;
|
|
@@ -86,7 +86,6 @@ export class IndexedGossipQueueMinSize<T extends {indexed?: string; queueAddedMs
|
|
|
86
86
|
const now = Date.now();
|
|
87
87
|
// here we mutate item, which is used for gossip validation later
|
|
88
88
|
item.indexed = key;
|
|
89
|
-
item.queueAddedMs = now;
|
|
90
89
|
let queueItem = this.indexedItems.get(key);
|
|
91
90
|
if (queueItem == null) {
|
|
92
91
|
queueItem = {firstSeenMs: now, listItems: new LinkedList<T>()};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {TopicValidatorResult} from "@libp2p/gossipsub";
|
|
1
2
|
import {routes} from "@lodestar/api";
|
|
2
3
|
import {ForkSeq} from "@lodestar/params";
|
|
3
4
|
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
|
|
@@ -560,6 +561,7 @@ export class NetworkProcessor {
|
|
|
560
561
|
|
|
561
562
|
private pushPendingGossipsubMessageToQueue(message: PendingGossipsubMessage): void {
|
|
562
563
|
const topicType = message.topic.type;
|
|
564
|
+
message.queueAddedMs = Date.now();
|
|
563
565
|
const droppedCount = this.gossipQueues[topicType].add(message);
|
|
564
566
|
if (droppedCount) {
|
|
565
567
|
// No need to report the dropped job to gossip. It will be eventually pruned from the mcache
|
|
@@ -746,12 +748,20 @@ export class NetworkProcessor {
|
|
|
746
748
|
for (const msg of messageOrArray) {
|
|
747
749
|
msg.startProcessUnixSec = nowSec;
|
|
748
750
|
if (msg.queueAddedMs !== undefined) {
|
|
749
|
-
this.metrics?.gossipValidationQueue.queueTime.observe(
|
|
751
|
+
this.metrics?.gossipValidationQueue.queueTime.observe(
|
|
752
|
+
{topic: msg.topic.type},
|
|
753
|
+
nowSec - msg.queueAddedMs / 1000
|
|
754
|
+
);
|
|
750
755
|
}
|
|
751
756
|
}
|
|
752
757
|
} else {
|
|
753
|
-
// indexed queue is not used here
|
|
754
758
|
messageOrArray.startProcessUnixSec = nowSec;
|
|
759
|
+
if (messageOrArray.queueAddedMs !== undefined) {
|
|
760
|
+
this.metrics?.gossipValidationQueue.queueTime.observe(
|
|
761
|
+
{topic: messageOrArray.topic.type},
|
|
762
|
+
nowSec - messageOrArray.queueAddedMs / 1000
|
|
763
|
+
);
|
|
764
|
+
}
|
|
755
765
|
}
|
|
756
766
|
|
|
757
767
|
const acceptanceArr = Array.isArray(messageOrArray)
|
|
@@ -764,11 +774,11 @@ export class NetworkProcessor {
|
|
|
764
774
|
];
|
|
765
775
|
|
|
766
776
|
if (Array.isArray(messageOrArray)) {
|
|
767
|
-
for (const msg of messageOrArray) {
|
|
768
|
-
this.trackJobTime(msg, messageOrArray.length);
|
|
777
|
+
for (const [i, msg] of messageOrArray.entries()) {
|
|
778
|
+
this.trackJobTime(msg, messageOrArray.length, acceptanceArr[i]);
|
|
769
779
|
}
|
|
770
780
|
} else {
|
|
771
|
-
this.trackJobTime(messageOrArray, 1);
|
|
781
|
+
this.trackJobTime(messageOrArray, 1, acceptanceArr[0]);
|
|
772
782
|
}
|
|
773
783
|
|
|
774
784
|
// Use setTimeout to yield to the macro queue
|
|
@@ -796,17 +806,21 @@ export class NetworkProcessor {
|
|
|
796
806
|
}
|
|
797
807
|
}
|
|
798
808
|
|
|
799
|
-
private trackJobTime(message: PendingGossipsubMessage, numJob: number): void {
|
|
809
|
+
private trackJobTime(message: PendingGossipsubMessage, numJob: number, acceptance: TopicValidatorResult): void {
|
|
800
810
|
if (message.startProcessUnixSec !== null) {
|
|
811
|
+
// record job wait time for all messages
|
|
801
812
|
this.metrics?.gossipValidationQueue.jobWaitTime.observe(
|
|
802
813
|
{topic: message.topic.type},
|
|
803
814
|
message.startProcessUnixSec - message.seenTimestampSec
|
|
804
815
|
);
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
816
|
+
// but only record job time for ACCEPT messages
|
|
817
|
+
if (acceptance === TopicValidatorResult.Accept) {
|
|
818
|
+
// if it takes 64ms to process 64 jobs, the average job time is 1ms
|
|
819
|
+
this.metrics?.gossipValidationQueue.jobTime.observe(
|
|
820
|
+
{topic: message.topic.type},
|
|
821
|
+
(Date.now() / 1000 - message.startProcessUnixSec) / numJob
|
|
822
|
+
);
|
|
823
|
+
}
|
|
810
824
|
}
|
|
811
825
|
}
|
|
812
826
|
|
package/src/sync/range/chain.ts
CHANGED
|
@@ -652,7 +652,9 @@ export class SyncChain {
|
|
|
652
652
|
|
|
653
653
|
const blockSlots = downloadSuccessOutput.blocks.map((b) => b.slot);
|
|
654
654
|
const envelopeSlots = downloadSuccessOutput.payloadEnvelopes
|
|
655
|
-
? Array.from(downloadSuccessOutput.payloadEnvelopes.
|
|
655
|
+
? Array.from(downloadSuccessOutput.payloadEnvelopes.entries())
|
|
656
|
+
.filter(([, payloadInput]) => payloadInput.hasPayloadEnvelope())
|
|
657
|
+
.map(([slot]) => slot)
|
|
656
658
|
: null;
|
|
657
659
|
|
|
658
660
|
this.logger.debug(logMessage, {
|
|
@@ -684,13 +686,18 @@ export class SyncChain {
|
|
|
684
686
|
*/
|
|
685
687
|
private async processBatch(batch: Batch): Promise<void> {
|
|
686
688
|
const {blocks, payloadEnvelopes, peers} = batch.startProcessing();
|
|
689
|
+
const envelopeSlots = payloadEnvelopes
|
|
690
|
+
? Array.from(payloadEnvelopes.entries())
|
|
691
|
+
.filter(([, payloadInput]) => payloadInput.hasPayloadEnvelope())
|
|
692
|
+
.map(([slot]) => slot)
|
|
693
|
+
: null;
|
|
687
694
|
|
|
688
695
|
const logCtx = {
|
|
689
696
|
id: this.logId,
|
|
690
697
|
...batch.getMetadata(),
|
|
691
698
|
blockCount: blocks.length,
|
|
692
699
|
blockSlots: prettyPrintIndices(blocks.map((b) => b.slot)),
|
|
693
|
-
...(
|
|
700
|
+
...(envelopeSlots ? {envelopeSlots: prettyPrintIndices(envelopeSlots)} : {}),
|
|
694
701
|
peers: peers.map(prettyPrintPeerIdStr).join(","),
|
|
695
702
|
};
|
|
696
703
|
this.logger.verbose("Processing batch", logCtx);
|
package/src/sync/range/range.ts
CHANGED
|
@@ -212,7 +212,7 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
212
212
|
for (const block of blocks) {
|
|
213
213
|
await this.chain.processBlock(block, flags);
|
|
214
214
|
const payloadEnvelope = payloadEnvelopes?.get(block.slot);
|
|
215
|
-
if (payloadEnvelope) {
|
|
215
|
+
if (payloadEnvelope?.hasPayloadEnvelope()) {
|
|
216
216
|
await this.chain.processExecutionPayload(payloadEnvelope);
|
|
217
217
|
}
|
|
218
218
|
}
|