@lodestar/beacon-node 1.46.0-rc.0 → 1.47.0-dev.d2f4b96574
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/chain/blocks/importBlock.js +1 -1
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.js +1 -1
- package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
- package/lib/chain/blocks/index.d.ts.map +1 -1
- package/lib/chain/blocks/index.js +3 -5
- 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 +11 -16
- package/lib/chain/blocks/verifyBlock.js.map +1 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +14 -4
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/options.d.ts +0 -2
- package/lib/chain/options.d.ts.map +1 -1
- package/lib/chain/options.js.map +1 -1
- package/lib/chain/prepareNextSlot.js +1 -1
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +2 -2
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/seenCache/seenBlockProposers.d.ts.map +1 -1
- package/lib/chain/seenCache/seenBlockProposers.js +7 -4
- package/lib/chain/seenCache/seenBlockProposers.js.map +1 -1
- package/lib/network/core/networkCore.d.ts.map +1 -1
- package/lib/network/core/networkCore.js +12 -1
- package/lib/network/core/networkCore.js.map +1 -1
- package/lib/network/core/networkCoreWorkerHandler.d.ts.map +1 -1
- package/lib/network/core/networkCoreWorkerHandler.js +20 -3
- package/lib/network/core/networkCoreWorkerHandler.js.map +1 -1
- package/lib/sync/range/batch.js +1 -1
- package/lib/sync/range/batch.js.map +1 -1
- package/lib/sync/range/utils/hashBlocks.d.ts +16 -4
- package/lib/sync/range/utils/hashBlocks.d.ts.map +1 -1
- package/lib/sync/range/utils/hashBlocks.js +49 -18
- package/lib/sync/range/utils/hashBlocks.js.map +1 -1
- package/lib/util/workerEvents.d.ts +7 -1
- package/lib/util/workerEvents.d.ts.map +1 -1
- package/lib/util/workerEvents.js +17 -5
- package/lib/util/workerEvents.js.map +1 -1
- package/package.json +17 -17
- package/src/chain/blocks/importBlock.ts +1 -1
- package/src/chain/blocks/importExecutionPayload.ts +1 -1
- package/src/chain/blocks/index.ts +3 -5
- package/src/chain/blocks/verifyBlock.ts +21 -25
- package/src/chain/chain.ts +15 -6
- package/src/chain/options.ts +0 -2
- package/src/chain/prepareNextSlot.ts +1 -1
- package/src/chain/produceBlock/produceBlockBody.ts +2 -2
- package/src/chain/seenCache/seenBlockProposers.ts +11 -4
- package/src/network/core/networkCore.ts +13 -1
- package/src/network/core/networkCoreWorkerHandler.ts +18 -3
- package/src/sync/range/batch.ts +1 -1
- package/src/sync/range/utils/hashBlocks.ts +56 -21
- package/src/util/workerEvents.ts +18 -6
|
@@ -250,7 +250,7 @@ export async function importExecutionPayload(
|
|
|
250
250
|
const head = this.forkChoice.getHead();
|
|
251
251
|
if (!this.opts.disableImportExecutionFcU && blockRootHex === head.blockRoot) {
|
|
252
252
|
const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
|
|
253
|
-
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice
|
|
253
|
+
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice);
|
|
254
254
|
this.executionEngine.notifyForkchoiceUpdate(fork, blockHashHex, safeBlockHash, finalizedBlockHash).catch((e) => {
|
|
255
255
|
if (!isErrorAborted(e) && !isQueueErrorAborted(e)) {
|
|
256
256
|
this.logger.error("Error pushing notifyForkchoiceUpdate()", {blockHashHex, finalizedBlockHash}, e);
|
|
@@ -108,11 +108,9 @@ export async function processBlocks(
|
|
|
108
108
|
throw segmentExecStatus.execAborted.execError;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.seenBlockProposers.add(block.slot, block.proposerIndex);
|
|
115
|
-
}
|
|
111
|
+
for (const blockInput of relevantBlocks) {
|
|
112
|
+
const block = blockInput.getBlock().message;
|
|
113
|
+
this.seenBlockProposers.add(block.slot, block.proposerIndex);
|
|
116
114
|
}
|
|
117
115
|
|
|
118
116
|
const {executionStatuses} = segmentExecStatus;
|
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
computeEpochAtSlot,
|
|
7
7
|
signedBlockToSignedHeader,
|
|
8
8
|
} from "@lodestar/state-transition";
|
|
9
|
-
import {IndexedAttestation, Slot, deneb
|
|
10
|
-
import {toRootHex} from "@lodestar/utils";
|
|
9
|
+
import {IndexedAttestation, Slot, deneb} from "@lodestar/types";
|
|
11
10
|
import {getBlobKzgCommitments} from "../../util/dataColumns.js";
|
|
11
|
+
import {callInNextEventLoop} from "../../util/eventLoop.js";
|
|
12
12
|
import type {BeaconChain} from "../chain.js";
|
|
13
13
|
import {BlockError, BlockErrorCode} from "../errors/index.js";
|
|
14
14
|
import {BlockProcessOpts} from "../options.js";
|
|
@@ -193,34 +193,30 @@ export async function verifyBlocksInEpoch(
|
|
|
193
193
|
),
|
|
194
194
|
|
|
195
195
|
// All signatures at once
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
)
|
|
207
|
-
: Promise.resolve({verifySignaturesTime: Date.now()}),
|
|
196
|
+
verifyBlocksSignatures(
|
|
197
|
+
this.config,
|
|
198
|
+
this.bls,
|
|
199
|
+
this.logger,
|
|
200
|
+
this.metrics,
|
|
201
|
+
preState0,
|
|
202
|
+
blocks,
|
|
203
|
+
indexedAttestationsByBlock,
|
|
204
|
+
opts
|
|
205
|
+
),
|
|
208
206
|
|
|
209
207
|
// TODO GLOAS: can verify payload signatures in batch too
|
|
210
208
|
// maybe chain with the above verifyBlocksSignatures()
|
|
211
209
|
]);
|
|
212
210
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.processProposerEquivocation(slot, proposerIndex);
|
|
223
|
-
}
|
|
211
|
+
for (const blockInput of blockInputs) {
|
|
212
|
+
const block = blockInput.getBlock();
|
|
213
|
+
const {slot, proposerIndex} = block.message;
|
|
214
|
+
const signedBlockHeader = signedBlockToSignedHeader(this.config, block);
|
|
215
|
+
this.seenBlockProposers.observeBlockRoot(slot, proposerIndex, blockInput.blockRootHex, signedBlockHeader);
|
|
216
|
+
// Only produce a slashing while importing the block. A block that is verified before it is published
|
|
217
|
+
// must not be treated as equivocation evidence since it may never be seen by the network
|
|
218
|
+
if (opts.verifyOnly !== true && this.seenBlockProposers.isEquivocating(slot, proposerIndex)) {
|
|
219
|
+
callInNextEventLoop(() => this.processProposerEquivocation(slot, proposerIndex));
|
|
224
220
|
}
|
|
225
221
|
}
|
|
226
222
|
|
package/src/chain/chain.ts
CHANGED
|
@@ -1174,11 +1174,15 @@ export class BeaconChain implements IBeaconChain {
|
|
|
1174
1174
|
|
|
1175
1175
|
/** Produce a proposer slashing from two conflicting signed block headers observed for the same slot and proposer */
|
|
1176
1176
|
private async produceProposerSlashing(blockSlot: Slot, proposerIndex: ValidatorIndex): Promise<void> {
|
|
1177
|
-
if (
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
) {
|
|
1177
|
+
if (this.opts.disableProposerSlashings === true || this.producingProposerSlashing.has(proposerIndex)) {
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
if (this.opPool.hasSeenProposerSlashing(proposerIndex)) {
|
|
1182
|
+
this.logger.debug("Not producing proposer slashing, already in the op pool", {
|
|
1183
|
+
slot: blockSlot,
|
|
1184
|
+
proposerIndex,
|
|
1185
|
+
});
|
|
1182
1186
|
return;
|
|
1183
1187
|
}
|
|
1184
1188
|
|
|
@@ -1213,7 +1217,12 @@ export class BeaconChain implements IBeaconChain {
|
|
|
1213
1217
|
this.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing);
|
|
1214
1218
|
this.emitter.emit(ChainEvent.publishProposerSlashing, proposerSlashing);
|
|
1215
1219
|
this.metrics?.opPool.proposerSlashingsProduced.inc();
|
|
1216
|
-
this.logger.info("Produced proposer slashing from observed equivocation", {
|
|
1220
|
+
this.logger.info("Produced proposer slashing from observed equivocation", {
|
|
1221
|
+
slot: blockSlot,
|
|
1222
|
+
proposerIndex,
|
|
1223
|
+
header1Root: toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(header1.message)),
|
|
1224
|
+
header2Root: toRootHex(ssz.phase0.BeaconBlockHeader.hashTreeRoot(header2.message)),
|
|
1225
|
+
});
|
|
1217
1226
|
} finally {
|
|
1218
1227
|
this.producingProposerSlashing.delete(proposerIndex);
|
|
1219
1228
|
}
|
package/src/chain/options.ts
CHANGED
|
@@ -86,8 +86,6 @@ export type BlockProcessOpts = {
|
|
|
86
86
|
verifyOnly?: boolean;
|
|
87
87
|
/** Used to specify to skip execution payload validation */
|
|
88
88
|
skipVerifyExecutionPayload?: boolean;
|
|
89
|
-
/** Used to specify to skip block signatures validation */
|
|
90
|
-
skipVerifyBlockSignatures?: boolean;
|
|
91
89
|
};
|
|
92
90
|
|
|
93
91
|
export type PoolOpts = {
|
|
@@ -224,7 +224,7 @@ export class PrepareNextSlotScheduler {
|
|
|
224
224
|
this.metrics?.blockPayload.payloadAdvancePrepTime.observe(preparationTime);
|
|
225
225
|
|
|
226
226
|
const safeBlockHash = getSafeExecutionBlockHash(this.chain.forkChoice, this.logger);
|
|
227
|
-
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.chain.forkChoice
|
|
227
|
+
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.chain.forkChoice);
|
|
228
228
|
|
|
229
229
|
// awaiting here instead of throwing an async call because there is no other task
|
|
230
230
|
// left for scheduler and this gives nice semantics to catch and log errors in the
|
|
@@ -271,7 +271,7 @@ export async function produceBlockBody<T extends BlockType>(
|
|
|
271
271
|
// full and blinded no longer makes sense in gloas, it might be a good idea to move
|
|
272
272
|
// this into a completely separate function and have pre/post gloas more separated
|
|
273
273
|
const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
|
|
274
|
-
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice
|
|
274
|
+
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice);
|
|
275
275
|
// TODO GLOAS: post-Gloas, proposer feeRecipient is also carried (signed) in
|
|
276
276
|
// ProposerPreferencesPool. Consider using this unified cache instead
|
|
277
277
|
// see https://github.com/ChainSafe/lodestar/issues/9379
|
|
@@ -417,7 +417,7 @@ export async function produceBlockBody<T extends BlockType>(
|
|
|
417
417
|
}
|
|
418
418
|
|
|
419
419
|
const safeBlockHash = getSafeExecutionBlockHash(this.forkChoice, this.logger);
|
|
420
|
-
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice
|
|
420
|
+
const finalizedBlockHash = getFinalizedExecutionBlockHash(this.forkChoice);
|
|
421
421
|
const feeRecipient = requestedFeeRecipient ?? this.beaconProposerCache.getOrDefault(proposerIndex);
|
|
422
422
|
const feeRecipientType = requestedFeeRecipient
|
|
423
423
|
? "requested"
|
|
@@ -3,7 +3,7 @@ import {Epoch, RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types";
|
|
|
3
3
|
import {MapDef} from "@lodestar/utils";
|
|
4
4
|
|
|
5
5
|
/** Two distinct block roots signed by the same proposer for the same slot are sufficient to establish an equivocation */
|
|
6
|
-
const
|
|
6
|
+
const MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL = 2;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Keeps a cache to filter block proposals from the same validator in the same slot.
|
|
@@ -36,7 +36,8 @@ export class SeenBlockProposers {
|
|
|
36
36
|
|
|
37
37
|
isEquivocating(blockSlot: Slot, proposerIndex: ValidatorIndex): boolean {
|
|
38
38
|
return (
|
|
39
|
-
(this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.size ?? 0) >=
|
|
39
|
+
(this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.size ?? 0) >=
|
|
40
|
+
MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -53,7 +54,10 @@ export class SeenBlockProposers {
|
|
|
53
54
|
proposerIndex: ValidatorIndex
|
|
54
55
|
): [phase0.SignedBeaconBlockHeader, phase0.SignedBeaconBlockHeader] | null {
|
|
55
56
|
const signedBlockHeaderByRoot = this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex);
|
|
56
|
-
if (
|
|
57
|
+
if (
|
|
58
|
+
signedBlockHeaderByRoot === undefined ||
|
|
59
|
+
signedBlockHeaderByRoot.size < MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL
|
|
60
|
+
) {
|
|
57
61
|
return null;
|
|
58
62
|
}
|
|
59
63
|
const [signedHeader1, signedHeader2] = signedBlockHeaderByRoot.values();
|
|
@@ -72,7 +76,10 @@ export class SeenBlockProposers {
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
const signedBlockHeaderByRoot = this.signedBlockHeadersBySlot.getOrDefault(blockSlot).getOrDefault(proposerIndex);
|
|
75
|
-
if (
|
|
79
|
+
if (
|
|
80
|
+
signedBlockHeaderByRoot.size < MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL &&
|
|
81
|
+
!signedBlockHeaderByRoot.has(blockRoot)
|
|
82
|
+
) {
|
|
76
83
|
signedBlockHeaderByRoot.set(blockRoot, signedBlockHeader);
|
|
77
84
|
}
|
|
78
85
|
}
|
|
@@ -70,6 +70,15 @@ export type BaseNetworkInit = {
|
|
|
70
70
|
initialCustodyGroupCount: number;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
/** Counts of what is still keeping this thread's event loop alive, e.g. `TCPWrap=3,Timeout=1` */
|
|
74
|
+
function formatActiveResources(): string {
|
|
75
|
+
const counts = new Map<string, number>();
|
|
76
|
+
for (const resource of process.getActiveResourcesInfo()) {
|
|
77
|
+
counts.set(resource, (counts.get(resource) ?? 0) + 1);
|
|
78
|
+
}
|
|
79
|
+
return Array.from(counts, ([name, count]) => `${name}=${count}`).join(",");
|
|
80
|
+
}
|
|
81
|
+
|
|
73
82
|
/**
|
|
74
83
|
* This class is meant to work both:
|
|
75
84
|
* - In a libp2p worker
|
|
@@ -289,7 +298,10 @@ export class NetworkCore implements INetworkCore {
|
|
|
289
298
|
this.attnetsService.close();
|
|
290
299
|
this.syncnetsService.close();
|
|
291
300
|
await this.libp2p.stop();
|
|
292
|
-
this
|
|
301
|
+
// Diagnostic for the shutdown hang, this thread can spin in `Environment::CleanupHandles()` on
|
|
302
|
+
// a handle that never closes and `Worker.terminate()` then never resolves. Diffing this list
|
|
303
|
+
// between a clean and a stuck shutdown should narrow down which handle it is
|
|
304
|
+
this.logger.debug("network lib2p closed", {activeResources: formatActiveResources()});
|
|
293
305
|
|
|
294
306
|
this.closed = true;
|
|
295
307
|
}
|
|
@@ -10,6 +10,7 @@ import {BeaconConfig, chainConfigToJson} from "@lodestar/config";
|
|
|
10
10
|
import type {LoggerNode} from "@lodestar/logger/node";
|
|
11
11
|
import {ResponseIncoming, ResponseOutgoing} from "@lodestar/reqresp";
|
|
12
12
|
import {Status} from "@lodestar/types";
|
|
13
|
+
import {withTimeout} from "@lodestar/utils";
|
|
13
14
|
import {Metrics} from "../../metrics/index.js";
|
|
14
15
|
import {AsyncIterableBridgeCaller, AsyncIterableBridgeHandler} from "../../util/asyncIterableToEvents.js";
|
|
15
16
|
import {PeerIdStr, peerIdFromString} from "../../util/peerId.js";
|
|
@@ -58,6 +59,10 @@ type WorkerNetworkCoreModules = WorkerNetworkCoreInitModules & {
|
|
|
58
59
|
|
|
59
60
|
const NETWORK_WORKER_EXIT_TIMEOUT_MS = 1000;
|
|
60
61
|
const NETWORK_WORKER_EXIT_RETRY_COUNT = 3;
|
|
62
|
+
/** `getApi().close()` is an unbounded RPC into the worker and runs before `BeaconNode.close()`
|
|
63
|
+
* archives chain state to disk, so it is bounded here. Set well above the ~2-4s a close takes on
|
|
64
|
+
* mainnet, tripping it leaves libp2p half torn down */
|
|
65
|
+
const NETWORK_CORE_CLOSE_TIMEOUT_MS = 5000;
|
|
61
66
|
|
|
62
67
|
/**
|
|
63
68
|
* NetworkCore implementation using a Worker thread
|
|
@@ -165,15 +170,25 @@ export class WorkerNetworkCore implements INetworkCore {
|
|
|
165
170
|
|
|
166
171
|
async close(): Promise<void> {
|
|
167
172
|
this.modules.logger.debug("closing network core running in network worker");
|
|
168
|
-
|
|
173
|
+
try {
|
|
174
|
+
await withTimeout(async () => this.getApi().close(), NETWORK_CORE_CLOSE_TIMEOUT_MS);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
this.modules.logger.debug("Error closing network core", {}, e as Error);
|
|
177
|
+
}
|
|
169
178
|
this.modules.logger.debug("terminating network worker");
|
|
170
|
-
await terminateWorkerThread({
|
|
179
|
+
const terminated = await terminateWorkerThread({
|
|
171
180
|
worker: this.getApi(),
|
|
172
181
|
retryCount: NETWORK_WORKER_EXIT_RETRY_COUNT,
|
|
173
182
|
retryMs: NETWORK_WORKER_EXIT_TIMEOUT_MS,
|
|
174
183
|
logger: this.modules.logger,
|
|
175
184
|
});
|
|
176
|
-
|
|
185
|
+
if (terminated) {
|
|
186
|
+
this.modules.logger.debug("terminated network worker");
|
|
187
|
+
} else {
|
|
188
|
+
// Nothing more to do, `process.exit()` joins the thread regardless so the process still waits
|
|
189
|
+
// on it. What matters is returning, so the caller can archive state and close the db
|
|
190
|
+
this.modules.logger.debug("Network worker did not terminate in time, continuing shutdown without it");
|
|
191
|
+
}
|
|
177
192
|
}
|
|
178
193
|
|
|
179
194
|
async test(): Promise<void> {
|
package/src/sync/range/batch.ts
CHANGED
|
@@ -593,7 +593,7 @@ export class Batch {
|
|
|
593
593
|
if (allComplete) {
|
|
594
594
|
const attempt: Attempt = {
|
|
595
595
|
peers: this.getSuccessfulPeers(),
|
|
596
|
-
hash: hashBlocks(blocks,
|
|
596
|
+
hash: hashBlocks(blocks, payloadEnvelopes),
|
|
597
597
|
};
|
|
598
598
|
this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes, attempt};
|
|
599
599
|
} else {
|
|
@@ -1,27 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {RootHex,
|
|
3
|
-
import {toRootHex} from "@lodestar/utils";
|
|
1
|
+
import {digest} from "@chainsafe/as-sha256";
|
|
2
|
+
import {RootHex, Slot, ssz} from "@lodestar/types";
|
|
3
|
+
import {fromHex, toRootHex} from "@lodestar/utils";
|
|
4
4
|
import {IBlockInput} from "../../../chain/blocks/blockInput/types.js";
|
|
5
|
+
import {PayloadEnvelopeInput} from "../../../chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js";
|
|
6
|
+
|
|
7
|
+
const ROOT_SIZE = 32;
|
|
8
|
+
const SIGNATURE_SIZE = 96;
|
|
9
|
+
/** Every block and every payload envelope contributes a fixed-size (root, signature) entry */
|
|
10
|
+
const ENTRY_SIZE = ROOT_SIZE + SIGNATURE_SIZE;
|
|
11
|
+
/** uint32 LE counts of block and envelope entries, so the variable-length sections are self-describing */
|
|
12
|
+
const COUNT_SIZE = 4;
|
|
13
|
+
const HEADER_SIZE = COUNT_SIZE * 2;
|
|
5
14
|
|
|
6
15
|
/**
|
|
7
|
-
*
|
|
16
|
+
* Root uniquely identifying a batch attempt (its blocks AND payloads). Used for peer scoring and to
|
|
17
|
+
* compare if two attempts are equivalent.
|
|
18
|
+
*
|
|
19
|
+
* Signatures are part of the id for BOTH blocks and payload envelopes, not just their message roots:
|
|
20
|
+
* a peer serving a correct message with a garbage signature would otherwise produce the same id as
|
|
21
|
+
* the honest attempt and escape scoring in `SyncChain.advanceChain`. Signatures are not verified at
|
|
22
|
+
* download time, only during processChainSegment (block signatures in verifyBlocksSignatures,
|
|
23
|
+
* envelope signatures in importExecutionPayload).
|
|
24
|
+
*
|
|
25
|
+
* Entries are written as raw bytes into a single exactly-sized buffer and digested once, so an
|
|
26
|
+
* `Attempt` retains 32 bytes rather than the ~16 KB a full 32-slot gloas batch would need if the
|
|
27
|
+
* roots and signatures were concatenated as hex. sha256 (not a 64-bit hash) because a peer that
|
|
28
|
+
* could force a collision with the winning attempt would escape peer scoring.
|
|
8
29
|
*/
|
|
9
|
-
export function hashBlocks(blocks: IBlockInput[],
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
export function hashBlocks(blocks: IBlockInput[], payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null): RootHex {
|
|
31
|
+
// Envelopes without a payload carry no attributable data, so they are skipped entirely. A `null`
|
|
32
|
+
// map and a map whose entries all lack a payload are therefore equivalent, which is intended.
|
|
33
|
+
const envelopes =
|
|
34
|
+
payloadEnvelopes && payloadEnvelopes.size > 0
|
|
35
|
+
? Array.from(payloadEnvelopes.entries())
|
|
36
|
+
.filter(([, envelope]) => envelope.hasPayloadEnvelope())
|
|
37
|
+
.sort(([slotA], [slotB]) => slotA - slotB)
|
|
38
|
+
: [];
|
|
39
|
+
|
|
40
|
+
const buf = Buffer.allocUnsafe(HEADER_SIZE + (blocks.length + envelopes.length) * ENTRY_SIZE);
|
|
41
|
+
buf.writeUInt32LE(blocks.length, 0);
|
|
42
|
+
buf.writeUInt32LE(envelopes.length, COUNT_SIZE);
|
|
43
|
+
let offset = HEADER_SIZE;
|
|
44
|
+
|
|
45
|
+
for (const block of blocks) {
|
|
46
|
+
buf.set(fromHex(block.blockRootHex), offset);
|
|
47
|
+
offset += ROOT_SIZE;
|
|
48
|
+
buf.set(block.getBlock().signature, offset);
|
|
49
|
+
offset += SIGNATURE_SIZE;
|
|
26
50
|
}
|
|
51
|
+
|
|
52
|
+
for (const [, envelope] of envelopes) {
|
|
53
|
+
const signedEnvelope = envelope.getPayloadEnvelope();
|
|
54
|
+
// envelope's root is cached thanks to cachePermanentRootStruct, so this avoids re-hashing
|
|
55
|
+
buf.set(ssz.gloas.ExecutionPayloadEnvelope.hashTreeRoot(signedEnvelope.message), offset);
|
|
56
|
+
offset += ROOT_SIZE;
|
|
57
|
+
buf.set(signedEnvelope.signature, offset);
|
|
58
|
+
offset += SIGNATURE_SIZE;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return toRootHex(digest(buf));
|
|
27
62
|
}
|
package/src/util/workerEvents.ts
CHANGED
|
@@ -111,6 +111,12 @@ export function wireEventsOnMainThread<EventData>(
|
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Terminate a worker thread, bounded to `retryCount * retryMs`.
|
|
116
|
+
*
|
|
117
|
+
* @returns `false` if it could not be terminated, the worker thread is then still running and the
|
|
118
|
+
* caller has to decide how to proceed without it.
|
|
119
|
+
*/
|
|
114
120
|
export async function terminateWorkerThread({
|
|
115
121
|
worker,
|
|
116
122
|
retryMs,
|
|
@@ -121,7 +127,7 @@ export async function terminateWorkerThread({
|
|
|
121
127
|
retryMs: number;
|
|
122
128
|
retryCount: number;
|
|
123
129
|
logger?: Logger;
|
|
124
|
-
}): Promise<
|
|
130
|
+
}): Promise<boolean> {
|
|
125
131
|
const terminated = new Promise((resolve) => {
|
|
126
132
|
Thread.events(worker).subscribe((event) => {
|
|
127
133
|
if (event.type === "termination") {
|
|
@@ -131,13 +137,19 @@ export async function terminateWorkerThread({
|
|
|
131
137
|
});
|
|
132
138
|
|
|
133
139
|
for (let i = 0; i < retryCount; i++) {
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
// `Worker.terminate()` resolves only once the worker thread exits, and the thread can spin
|
|
141
|
+
// forever in `Environment::CleanupHandles()` when a libuv handle on its loop never closes, so
|
|
142
|
+
// it has to be raced too, otherwise `retryCount * retryMs` never applies.
|
|
143
|
+
const result = await Promise.race([
|
|
144
|
+
Thread.terminate(worker).then(() => terminated),
|
|
145
|
+
sleep(retryMs).then(() => false),
|
|
146
|
+
]);
|
|
136
147
|
|
|
137
|
-
if (result) return;
|
|
148
|
+
if (result) return true;
|
|
138
149
|
|
|
139
|
-
logger?.
|
|
150
|
+
logger?.debug("Worker thread failed to terminate, retrying...");
|
|
140
151
|
}
|
|
141
152
|
|
|
142
|
-
|
|
153
|
+
logger?.debug(`Worker thread failed to terminate in ${retryCount * retryMs}ms`);
|
|
154
|
+
return false;
|
|
143
155
|
}
|