@lodestar/beacon-node 1.51.0-dev.ec493d509d → 1.51.0-dev.f33a955402
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 +11 -22
- 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 +4 -1
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/chain/GetBlobsTracker.d.ts +11 -0
- package/lib/chain/GetBlobsTracker.d.ts.map +1 -1
- package/lib/chain/GetBlobsTracker.js +83 -27
- package/lib/chain/GetBlobsTracker.js.map +1 -1
- package/lib/chain/archiveStore/utils/archiveBlocks.d.ts +4 -0
- package/lib/chain/archiveStore/utils/archiveBlocks.d.ts.map +1 -1
- package/lib/chain/archiveStore/utils/archiveBlocks.js +7 -1
- package/lib/chain/archiveStore/utils/archiveBlocks.js.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.d.ts +4 -1
- package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.js +2 -1
- package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
- package/lib/chain/blocks/index.js +2 -2
- package/lib/chain/blocks/index.js.map +1 -1
- package/lib/chain/blocks/payloadEnvelopeProcessor.d.ts +5 -5
- package/lib/chain/blocks/payloadEnvelopeProcessor.js +5 -5
- package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
- package/lib/chain/blocks/verifyBlock.js +1 -1
- package/lib/chain/blocks/verifyBlock.js.map +1 -1
- package/lib/chain/blocks/verifyBlocksDataAvailability.d.ts.map +1 -1
- package/lib/chain/blocks/verifyBlocksDataAvailability.js +8 -1
- package/lib/chain/blocks/verifyBlocksDataAvailability.js.map +1 -1
- package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts +3 -3
- package/lib/chain/blocks/verifyPayloadsDataAvailability.d.ts.map +1 -1
- package/lib/chain/blocks/verifyPayloadsDataAvailability.js +13 -6
- package/lib/chain/blocks/verifyPayloadsDataAvailability.js.map +1 -1
- package/lib/chain/errors/blockError.d.ts +1 -1
- package/lib/chain/errors/blockError.d.ts.map +1 -1
- package/lib/chain/errors/blockError.js +1 -1
- package/lib/chain/errors/blockError.js.map +1 -1
- package/lib/chain/options.d.ts +0 -1
- package/lib/chain/options.d.ts.map +1 -1
- package/lib/chain/options.js +0 -1
- package/lib/chain/options.js.map +1 -1
- package/lib/db/beacon.d.ts +7 -0
- package/lib/db/beacon.d.ts.map +1 -1
- package/lib/db/beacon.js +32 -0
- package/lib/db/beacon.js.map +1 -1
- package/lib/db/dataColumnStore.d.ts +1 -1
- package/lib/db/dataColumnStore.d.ts.map +1 -1
- package/lib/db/dataColumnStore.js +1 -4
- package/lib/db/dataColumnStore.js.map +1 -1
- package/lib/db/interface.d.ts +1 -0
- package/lib/db/interface.d.ts.map +1 -1
- package/lib/db/repositories/dataColumnSidecarArchive.d.ts +6 -0
- package/lib/db/repositories/dataColumnSidecarArchive.d.ts.map +1 -1
- package/lib/db/repositories/dataColumnSidecarArchive.js +19 -0
- package/lib/db/repositories/dataColumnSidecarArchive.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +6 -3
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/node/nodejs.d.ts.map +1 -1
- package/lib/node/nodejs.js +9 -1
- package/lib/node/nodejs.js.map +1 -1
- package/package.json +15 -15
- package/src/api/impl/beacon/blocks/index.ts +12 -24
- package/src/api/impl/validator/index.ts +4 -1
- package/src/chain/GetBlobsTracker.ts +95 -38
- package/src/chain/archiveStore/utils/archiveBlocks.ts +12 -4
- package/src/chain/blocks/importExecutionPayload.ts +5 -1
- package/src/chain/blocks/index.ts +6 -2
- package/src/chain/blocks/payloadEnvelopeProcessor.ts +5 -5
- package/src/chain/blocks/verifyBlock.ts +1 -0
- package/src/chain/blocks/verifyBlocksDataAvailability.ts +10 -1
- package/src/chain/blocks/verifyPayloadsDataAvailability.ts +16 -6
- package/src/chain/errors/blockError.ts +1 -1
- package/src/chain/options.ts +0 -2
- package/src/db/beacon.ts +39 -0
- package/src/db/dataColumnStore.ts +2 -4
- package/src/db/interface.ts +2 -0
- package/src/db/repositories/dataColumnSidecarArchive.ts +20 -0
- package/src/network/processor/gossipHandlers.ts +6 -3
- package/src/node/nodejs.ts +18 -1
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"bugs": {
|
|
12
12
|
"url": "https://github.com/ChainSafe/lodestar/issues"
|
|
13
13
|
},
|
|
14
|
-
"version": "1.51.0-dev.
|
|
14
|
+
"version": "1.51.0-dev.f33a955402",
|
|
15
15
|
"type": "module",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
@@ -127,17 +127,17 @@
|
|
|
127
127
|
"@libp2p/prometheus-metrics": "^5.0.31",
|
|
128
128
|
"@libp2p/tcp": "^11.0.30",
|
|
129
129
|
"@libp2p/utils": "^7.4.3",
|
|
130
|
-
"@lodestar/api": "^1.51.0-dev.
|
|
131
|
-
"@lodestar/config": "^1.51.0-dev.
|
|
132
|
-
"@lodestar/db": "^1.51.0-dev.
|
|
133
|
-
"@lodestar/fork-choice": "^1.51.0-dev.
|
|
134
|
-
"@lodestar/logger": "^1.51.0-dev.
|
|
135
|
-
"@lodestar/params": "^1.51.0-dev.
|
|
136
|
-
"@lodestar/reqresp": "^1.51.0-dev.
|
|
137
|
-
"@lodestar/state-transition": "^1.51.0-dev.
|
|
138
|
-
"@lodestar/types": "^1.51.0-dev.
|
|
139
|
-
"@lodestar/utils": "^1.51.0-dev.
|
|
140
|
-
"@lodestar/validator": "^1.51.0-dev.
|
|
130
|
+
"@lodestar/api": "^1.51.0-dev.f33a955402",
|
|
131
|
+
"@lodestar/config": "^1.51.0-dev.f33a955402",
|
|
132
|
+
"@lodestar/db": "^1.51.0-dev.f33a955402",
|
|
133
|
+
"@lodestar/fork-choice": "^1.51.0-dev.f33a955402",
|
|
134
|
+
"@lodestar/logger": "^1.51.0-dev.f33a955402",
|
|
135
|
+
"@lodestar/params": "^1.51.0-dev.f33a955402",
|
|
136
|
+
"@lodestar/reqresp": "^1.51.0-dev.f33a955402",
|
|
137
|
+
"@lodestar/state-transition": "^1.51.0-dev.f33a955402",
|
|
138
|
+
"@lodestar/types": "^1.51.0-dev.f33a955402",
|
|
139
|
+
"@lodestar/utils": "^1.51.0-dev.f33a955402",
|
|
140
|
+
"@lodestar/validator": "^1.51.0-dev.f33a955402",
|
|
141
141
|
"@multiformats/multiaddr": "^13.0.1",
|
|
142
142
|
"datastore-core": "^12.0.1",
|
|
143
143
|
"datastore-level": "^13.0.1",
|
|
@@ -159,8 +159,8 @@
|
|
|
159
159
|
"@ethereumjs/rlp": "^4.0.1",
|
|
160
160
|
"@libp2p/interface-internal": "^3.2.0",
|
|
161
161
|
"@libp2p/logger": "^6.2.15",
|
|
162
|
-
"@lodestar/spec-test-util": "^1.51.0-dev.
|
|
163
|
-
"@lodestar/test-utils": "^1.51.0-dev.
|
|
162
|
+
"@lodestar/spec-test-util": "^1.51.0-dev.f33a955402",
|
|
163
|
+
"@lodestar/test-utils": "^1.51.0-dev.f33a955402",
|
|
164
164
|
"@types/js-yaml": "^4.0.5",
|
|
165
165
|
"@types/qs": "^6.9.7",
|
|
166
166
|
"@types/tmp": "^0.2.3",
|
|
@@ -175,5 +175,5 @@
|
|
|
175
175
|
"beacon",
|
|
176
176
|
"blockchain"
|
|
177
177
|
],
|
|
178
|
-
"gitHead": "
|
|
178
|
+
"gitHead": "175cb2656b8e95668eac3b1fa4e2874455f6c58f"
|
|
179
179
|
}
|
|
@@ -316,18 +316,8 @@ export function getBeaconBlockApi(
|
|
|
316
316
|
break;
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
-
case routes.beacon.BroadcastValidation.none: {
|
|
320
|
-
chain.logger.debug("Skipping broadcast validation", valLogMeta);
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
319
|
default: {
|
|
325
|
-
|
|
326
|
-
const message = `Broadcast validation of ${broadcastValidation} type not implemented yet`;
|
|
327
|
-
if (chain.opts.broadcastValidationStrictness === "error") {
|
|
328
|
-
throw Error(message);
|
|
329
|
-
}
|
|
330
|
-
chain.logger.warn(message, valLogMeta);
|
|
320
|
+
throw new ApiError(400, `Invalid broadcast validation level ${broadcastValidation}`);
|
|
331
321
|
}
|
|
332
322
|
}
|
|
333
323
|
|
|
@@ -804,11 +794,6 @@ export function getBeaconBlockApi(
|
|
|
804
794
|
};
|
|
805
795
|
try {
|
|
806
796
|
switch (broadcastValidation) {
|
|
807
|
-
case routes.beacon.BroadcastValidation.none: {
|
|
808
|
-
chain.logger.debug("Skipping broadcast validation of execution payload envelope", valLogMeta);
|
|
809
|
-
break;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
797
|
case routes.beacon.BroadcastValidation.gossip: {
|
|
813
798
|
await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope);
|
|
814
799
|
break;
|
|
@@ -847,11 +832,7 @@ export function getBeaconBlockApi(
|
|
|
847
832
|
}
|
|
848
833
|
|
|
849
834
|
default: {
|
|
850
|
-
|
|
851
|
-
if (chain.opts.broadcastValidationStrictness === "error") {
|
|
852
|
-
throw Error(message);
|
|
853
|
-
}
|
|
854
|
-
chain.logger.warn(message, valLogMeta);
|
|
835
|
+
throw new ApiError(400, `Invalid broadcast validation level ${broadcastValidation}`);
|
|
855
836
|
}
|
|
856
837
|
}
|
|
857
838
|
} catch (error) {
|
|
@@ -1004,7 +985,7 @@ export function getBeaconBlockApi(
|
|
|
1004
985
|
// Publish all data column sidecars
|
|
1005
986
|
...dataColumnSidecars.map((dataColumnSidecar) => () => network.publishDataColumnSidecar(dataColumnSidecar)),
|
|
1006
987
|
// Import execution payload. Signature is verified during broadcast validation, an already
|
|
1007
|
-
// known envelope was verified by its original source
|
|
988
|
+
// known envelope was verified by its original source
|
|
1008
989
|
() => chain.processExecutionPayload(payloadInput, {validSignature: true}),
|
|
1009
990
|
];
|
|
1010
991
|
|
|
@@ -1109,7 +1090,7 @@ export function getBeaconBlockApi(
|
|
|
1109
1090
|
},
|
|
1110
1091
|
|
|
1111
1092
|
async getSignedExecutionPayloadEnvelope({blockId}, context) {
|
|
1112
|
-
const {block,
|
|
1093
|
+
const {block, finalized} = await getBlockResponse(chain, blockId);
|
|
1113
1094
|
const slot = block.message.slot;
|
|
1114
1095
|
const fork = config.getForkName(slot);
|
|
1115
1096
|
|
|
@@ -1123,6 +1104,13 @@ export function getBeaconBlockApi(
|
|
|
1123
1104
|
const blockRoot = config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message);
|
|
1124
1105
|
const blockRootHex = toRootHex(blockRoot);
|
|
1125
1106
|
|
|
1107
|
+
// The block's own flag is the status of the payload it builds on, the envelope's is that of its
|
|
1108
|
+
// FULL variant. An unfinalized block without one has not imported its envelope yet.
|
|
1109
|
+
const payload = finalized ? null : chain.forkChoice.getBlockHex(blockRootHex, PayloadStatus.FULL);
|
|
1110
|
+
if (!finalized && payload === null) {
|
|
1111
|
+
throw new ApiError(404, `Execution payload envelope not found for slot=${slot}, blockRoot=${blockRootHex}`);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1126
1114
|
let data: Uint8Array | gloas.SignedExecutionPayloadEnvelope | null;
|
|
1127
1115
|
try {
|
|
1128
1116
|
data = context?.returnBytes
|
|
@@ -1147,7 +1135,7 @@ export function getBeaconBlockApi(
|
|
|
1147
1135
|
|
|
1148
1136
|
return {
|
|
1149
1137
|
data,
|
|
1150
|
-
meta: {executionOptimistic, finalized, version: fork},
|
|
1138
|
+
meta: {executionOptimistic: payload !== null && isOptimisticBlock(payload), finalized, version: fork},
|
|
1151
1139
|
};
|
|
1152
1140
|
},
|
|
1153
1141
|
|
|
@@ -1756,7 +1756,10 @@ export function getValidatorApi(
|
|
|
1756
1756
|
}
|
|
1757
1757
|
}
|
|
1758
1758
|
|
|
1759
|
-
|
|
1759
|
+
// The fork epoch PTC is computed at the fork from effective balances updated at the end of the
|
|
1760
|
+
// last pre-fork epoch, so its dependent root is one epoch later than for other epochs
|
|
1761
|
+
const dependentRootEpoch = epoch === config.GLOAS_FORK_EPOCH ? epoch + 1 : epoch;
|
|
1762
|
+
const dependentRoot = fromHex(state.getShufflingDecisionRoot(dependentRootEpoch));
|
|
1760
1763
|
|
|
1761
1764
|
return {
|
|
1762
1765
|
data: duties,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import {ChainForkConfig} from "@lodestar/config";
|
|
2
2
|
import {computeEpochAtSlot} from "@lodestar/state-transition";
|
|
3
|
-
import {Logger} from "@lodestar/utils";
|
|
3
|
+
import {Logger, pruneSetToMax} from "@lodestar/utils";
|
|
4
4
|
import {BLOB_AND_PROOF_V2_RPC_BYTES} from "../execution/engine/types.js";
|
|
5
5
|
import {IExecutionEngine} from "../execution/index.js";
|
|
6
6
|
import {Metrics} from "../metrics/metrics.js";
|
|
7
|
-
import {callInNextEventLoop} from "../util/eventLoop.js";
|
|
8
7
|
import {
|
|
9
8
|
DataColumnEngineResult,
|
|
10
9
|
getBlobSidecarsFromExecution,
|
|
@@ -14,6 +13,11 @@ import {IBlockInput, isBlockInputBlobs} from "./blocks/blockInput/index.js";
|
|
|
14
13
|
import {PayloadEnvelopeInput} from "./blocks/payloadEnvelopeInput/index.js";
|
|
15
14
|
import {ChainEventEmitter} from "./emitter.js";
|
|
16
15
|
|
|
16
|
+
/** A `null` answer only changes if the EL receives the transactions later, so retries are few and spaced out */
|
|
17
|
+
export const MAX_GET_BLOBS_ATTEMPTS = 3;
|
|
18
|
+
export const GET_BLOBS_RETRY_INTERVAL_MS = 1000;
|
|
19
|
+
const MAX_TRACKED_BLOCK_ROOTS = 64;
|
|
20
|
+
|
|
17
21
|
export type GetBlobsTrackerInit = {
|
|
18
22
|
logger: Logger;
|
|
19
23
|
executionEngine: IExecutionEngine;
|
|
@@ -32,6 +36,8 @@ export class GetBlobsTracker {
|
|
|
32
36
|
metrics: Metrics | null;
|
|
33
37
|
config: ChainForkConfig;
|
|
34
38
|
activeReconstructions = new Set<string>();
|
|
39
|
+
failedAttempts = new Map<string, {count: number; lastAttemptMs: number; retryTimer?: NodeJS.Timeout}>();
|
|
40
|
+
pendingTriggers = new Set<string>();
|
|
35
41
|
// Preallocate buffers for getBlobsV2 RPC calls
|
|
36
42
|
// See https://github.com/ChainSafe/lodestar/pull/8282 for context
|
|
37
43
|
blobsAndProofsBuffers: {buffers: Uint8Array[]; inUse: boolean}[] = [];
|
|
@@ -46,23 +52,55 @@ export class GetBlobsTracker {
|
|
|
46
52
|
|
|
47
53
|
triggerGetBlobs(input: IBlockInput | PayloadEnvelopeInput): void {
|
|
48
54
|
if (this.activeReconstructions.has(input.blockRootHex)) {
|
|
55
|
+
this.pendingTriggers.add(input.blockRootHex);
|
|
49
56
|
return;
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
const failed = this.failedAttempts.get(input.blockRootHex);
|
|
60
|
+
if (failed) {
|
|
61
|
+
if (failed.count >= MAX_GET_BLOBS_ATTEMPTS) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const waitMs = GET_BLOBS_RETRY_INTERVAL_MS - (Date.now() - failed.lastAttemptMs);
|
|
65
|
+
if (waitMs > 0) {
|
|
66
|
+
// Columns tend to arrive in one burst, so keep a single trailing retry instead of relying on a later trigger
|
|
67
|
+
failed.retryTimer ??= setTimeout(() => {
|
|
68
|
+
failed.retryTimer = undefined;
|
|
69
|
+
if (this.failedAttempts.get(input.blockRootHex) === failed && !input.hasAllData()) {
|
|
70
|
+
this.triggerGetBlobs(input);
|
|
71
|
+
}
|
|
72
|
+
}, waitMs).unref();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The request is sent right away, before block processing issues newPayload: ELs that drop a
|
|
78
|
+
// transaction's blobs once the payload is validated can only answer a request that gets there first
|
|
52
79
|
if (!(input instanceof PayloadEnvelopeInput) && isBlockInputBlobs(input)) {
|
|
53
80
|
// there is not preallocation for blob sidecars like there is for columns sidecars so no need to
|
|
54
81
|
// store the index for the preallocated buffers
|
|
55
82
|
this.activeReconstructions.add(input.blockRootHex);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
const logCtx = {slot: input.slot, root: input.blockRootHex};
|
|
84
|
+
this.logger.verbose("Trigger getBlobsV1 for block", logCtx);
|
|
85
|
+
let requestFailed = false;
|
|
86
|
+
getBlobSidecarsFromExecution(this.config, this.executionEngine, this.metrics, this.emitter, input)
|
|
87
|
+
.then(() => {
|
|
88
|
+
// EL errors and misses are handled inside the helper, blobs still missing means the attempt failed
|
|
89
|
+
if (!input.hasAllData()) {
|
|
90
|
+
requestFailed = true;
|
|
91
|
+
this.recordFailedAttempt(input.blockRootHex);
|
|
63
92
|
}
|
|
64
|
-
)
|
|
65
|
-
|
|
93
|
+
})
|
|
94
|
+
.catch((error) => {
|
|
95
|
+
this.logger.debug("Error during getBlobsV1 for block", logCtx, error as Error);
|
|
96
|
+
requestFailed = true;
|
|
97
|
+
this.recordFailedAttempt(input.blockRootHex);
|
|
98
|
+
})
|
|
99
|
+
.finally(() => {
|
|
100
|
+
this.logger.verbose("Completed getBlobsV1 for block", logCtx);
|
|
101
|
+
this.activeReconstructions.delete(input.blockRootHex);
|
|
102
|
+
this.retryPendingTrigger(input, requestFailed);
|
|
103
|
+
});
|
|
66
104
|
|
|
67
105
|
return;
|
|
68
106
|
}
|
|
@@ -83,34 +121,53 @@ export class GetBlobsTracker {
|
|
|
83
121
|
}
|
|
84
122
|
timer?.();
|
|
85
123
|
|
|
86
|
-
// We don't care about the outcome of this call,
|
|
87
|
-
// just that it has been triggered for this block root.
|
|
88
124
|
this.activeReconstructions.add(input.blockRootHex);
|
|
89
125
|
this.blobsAndProofsBuffers[freeIndex].inUse = true;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
126
|
+
const logCtx = {slot: input.slot, root: input.blockRootHex};
|
|
127
|
+
this.logger.verbose("Trigger getBlobsV2 for block", logCtx);
|
|
128
|
+
let requestFailed = false;
|
|
129
|
+
getDataColumnSidecarsFromExecution(
|
|
130
|
+
this.config,
|
|
131
|
+
this.executionEngine,
|
|
132
|
+
this.emitter,
|
|
133
|
+
input,
|
|
134
|
+
this.metrics,
|
|
135
|
+
this.blobsAndProofsBuffers[freeIndex].buffers
|
|
136
|
+
)
|
|
137
|
+
.then((result) => {
|
|
138
|
+
this.logger.debug("getBlobsV2 result for block", {...logCtx, result});
|
|
139
|
+
this.metrics?.dataColumns.dataColumnEngineResult.inc({result});
|
|
140
|
+
if (result === DataColumnEngineResult.NullResponse) {
|
|
141
|
+
requestFailed = true;
|
|
142
|
+
this.recordFailedAttempt(input.blockRootHex);
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
.catch((error) => {
|
|
146
|
+
this.logger.debug("Error during getBlobsV2 for block", logCtx, error as Error);
|
|
147
|
+
this.metrics?.dataColumns.dataColumnEngineResult.inc({result: DataColumnEngineResult.Failed});
|
|
148
|
+
requestFailed = true;
|
|
149
|
+
this.recordFailedAttempt(input.blockRootHex);
|
|
150
|
+
})
|
|
151
|
+
.finally(() => {
|
|
152
|
+
this.logger.verbose("Completed getBlobsV2 for block", logCtx);
|
|
153
|
+
this.activeReconstructions.delete(input.blockRootHex);
|
|
154
|
+
this.blobsAndProofsBuffers[freeIndex].inUse = false;
|
|
155
|
+
this.retryPendingTrigger(input, requestFailed);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private retryPendingTrigger(input: IBlockInput | PayloadEnvelopeInput, requestFailed: boolean): void {
|
|
160
|
+
// Triggers dropped while the request was in flight re-enter once it failed, the attempt that was just
|
|
161
|
+
// recorded makes this arm the trailing retry instead of sending another request right away
|
|
162
|
+
if (this.pendingTriggers.delete(input.blockRootHex) && requestFailed) {
|
|
163
|
+
this.triggerGetBlobs(input);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private recordFailedAttempt(blockRootHex: string): void {
|
|
168
|
+
const failed = this.failedAttempts.get(blockRootHex);
|
|
169
|
+
clearTimeout(failed?.retryTimer);
|
|
170
|
+
this.failedAttempts.set(blockRootHex, {count: (failed?.count ?? 0) + 1, lastAttemptMs: Date.now()});
|
|
171
|
+
pruneSetToMax(this.failedAttempts, MAX_TRACKED_BLOCK_ROOTS);
|
|
115
172
|
}
|
|
116
173
|
}
|
|
@@ -56,6 +56,17 @@ async function persistOrphanedBlock(
|
|
|
56
56
|
await writeIfNotExist(filepath, bytes);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Oldest epoch for which data column sidecars are kept
|
|
61
|
+
*/
|
|
62
|
+
export function getDataColumnSidecarsMinEpoch(
|
|
63
|
+
config: ChainForkConfig,
|
|
64
|
+
currentEpoch: Epoch,
|
|
65
|
+
archiveDataEpochs?: number
|
|
66
|
+
): Epoch {
|
|
67
|
+
return Math.min(getBlobDataRetentionStartEpoch(config, currentEpoch), currentEpoch - (archiveDataEpochs ?? 0));
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
/**
|
|
60
71
|
* Archives finalized blocks from active bucket to archive bucket.
|
|
61
72
|
*
|
|
@@ -260,10 +271,7 @@ export async function archiveBlocks(
|
|
|
260
271
|
// Keep only `[min(compute_blob_data_retention_start_epoch(current_epoch), current_epoch - archiveDataEpochs)]`
|
|
261
272
|
if (finalizedPostFulu) {
|
|
262
273
|
if (archiveDataEpochs !== Infinity) {
|
|
263
|
-
const dataColumnSidecarsMinEpoch =
|
|
264
|
-
getBlobDataRetentionStartEpoch(config, currentEpoch),
|
|
265
|
-
currentEpoch - (archiveDataEpochs ?? 0)
|
|
266
|
-
);
|
|
274
|
+
const dataColumnSidecarsMinEpoch = getDataColumnSidecarsMinEpoch(config, currentEpoch, archiveDataEpochs);
|
|
267
275
|
if (dataColumnSidecarsMinEpoch >= config.FULU_FORK_EPOCH) {
|
|
268
276
|
const columnsPruneSlot = computeStartSlotAtEpoch(dataColumnSidecarsMinEpoch);
|
|
269
277
|
const prunedColumnSlots = await db.dataColumns.pruneBefore(columnsPruneSlot);
|
|
@@ -37,6 +37,7 @@ export enum PayloadErrorCode {
|
|
|
37
37
|
MISS_BLOCK_STATE = "PAYLOAD_ERROR_MISS_BLOCK_STATE",
|
|
38
38
|
ENVELOPE_VERIFICATION_ERROR = "PAYLOAD_ERROR_ENVELOPE_VERIFICATION_ERROR",
|
|
39
39
|
INVALID_SIGNATURE = "PAYLOAD_ERROR_INVALID_SIGNATURE",
|
|
40
|
+
DATA_UNAVAILABLE = "PAYLOAD_ERROR_DATA_UNAVAILABLE",
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export type PayloadErrorType =
|
|
@@ -64,6 +65,9 @@ export type PayloadErrorType =
|
|
|
64
65
|
}
|
|
65
66
|
| {
|
|
66
67
|
code: PayloadErrorCode.INVALID_SIGNATURE;
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
code: PayloadErrorCode.DATA_UNAVAILABLE;
|
|
67
71
|
};
|
|
68
72
|
|
|
69
73
|
export class PayloadError extends Error {
|
|
@@ -383,6 +387,6 @@ export async function processExecutionPayload(
|
|
|
383
387
|
signal: AbortSignal,
|
|
384
388
|
opts: ImportPayloadOpts = {}
|
|
385
389
|
): Promise<void> {
|
|
386
|
-
const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability([payloadInput], signal);
|
|
390
|
+
const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(this.config, [payloadInput], signal);
|
|
387
391
|
await importExecutionPayload.call(this, payloadInput, dataAvailabilityStatuses[0], opts);
|
|
388
392
|
}
|
|
@@ -147,7 +147,7 @@ export async function processBlocks(
|
|
|
147
147
|
proposerBalanceDelta: proposerBalanceDeltas[i],
|
|
148
148
|
indexedAttestations: indexedAttestationsByBlock[i],
|
|
149
149
|
// TODO: Make this param mandatory and capture in gossip
|
|
150
|
-
seenTimestampSec: opts.seenTimestampSec ??
|
|
150
|
+
seenTimestampSec: opts.seenTimestampSec ?? Date.now() / 1000,
|
|
151
151
|
});
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -292,7 +292,11 @@ async function importPayloadEnvelopesOfKnownBlocks(
|
|
|
292
292
|
return result;
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(
|
|
295
|
+
const {dataAvailabilityStatuses} = await verifyPayloadsDataAvailability(
|
|
296
|
+
this.config,
|
|
297
|
+
payloadInputs,
|
|
298
|
+
new AbortController().signal
|
|
299
|
+
);
|
|
296
300
|
for (let i = 0; i < payloadInputs.length; i++) {
|
|
297
301
|
this.logger.debug("Importing payload envelope of known block", {
|
|
298
302
|
slot: payloadInputs[i].slot,
|
|
@@ -13,11 +13,11 @@ const QUEUE_MAX_LENGTH = 16;
|
|
|
13
13
|
*
|
|
14
14
|
* Jobs are enqueued only on envelope arrival (gossip or API). The envelope may reach us before
|
|
15
15
|
* the sampled data columns; importExecutionPayload awaits `verifyPayloadsDataAvailability`
|
|
16
|
-
* internally, so a queued job can pend for up to
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* internally, so a queued job can pend for up to one slot while waiting for columns. Duplicate
|
|
17
|
+
* triggers for the same payloadInput are deduped by sharing the in-flight import promise: every
|
|
18
|
+
* caller observes the real import outcome. Resolving duplicates early instead would let
|
|
19
|
+
* BlockInputSync mark a still-pending payload as processed, re-queue it, and spin re-processing
|
|
20
|
+
* the same root until OOM while the import is blocked (e.g. EL syncing).
|
|
21
21
|
*/
|
|
22
22
|
export class PayloadEnvelopeProcessor {
|
|
23
23
|
readonly jobQueue: JobItemQueue<[PayloadEnvelopeInput, ImportPayloadOpts], void>;
|
|
@@ -143,6 +143,7 @@ export async function verifyBlocksInEpoch(
|
|
|
143
143
|
? Array.from(payloadEnvelopes.values()).filter((payloadInput) => payloadInput.hasPayloadEnvelope())
|
|
144
144
|
: [];
|
|
145
145
|
const {dataAvailabilityStatuses, availableTime} = await verifyPayloadsDataAvailability(
|
|
146
|
+
this.config,
|
|
146
147
|
payloadInputsForDa,
|
|
147
148
|
abortController.signal
|
|
148
149
|
);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import {DataAvailabilityStatus} from "@lodestar/state-transition";
|
|
2
|
+
import {TimeoutError} from "@lodestar/utils";
|
|
3
|
+
import {BlockError, BlockErrorCode} from "../errors/index.js";
|
|
2
4
|
import {DAData, DAType, IBlockInput} from "./blockInput/index.js";
|
|
3
5
|
|
|
4
6
|
// we can now wait for full 12 seconds because unavailable block sync will try pulling
|
|
@@ -22,7 +24,14 @@ export async function verifyBlocksDataAvailability(
|
|
|
22
24
|
for (const blockInput of blocks) {
|
|
23
25
|
// block verification is triggered on a verified gossip block so we only need to wait for all data
|
|
24
26
|
if (!blockInput.hasAllData()) {
|
|
25
|
-
promises.push(
|
|
27
|
+
promises.push(
|
|
28
|
+
blockInput.waitForAllData(BLOB_AVAILABILITY_TIMEOUT, signal).catch((e) => {
|
|
29
|
+
if (e instanceof TimeoutError) {
|
|
30
|
+
throw new BlockError(blockInput.getBlock(), {code: BlockErrorCode.DATA_UNAVAILABLE});
|
|
31
|
+
}
|
|
32
|
+
throw e;
|
|
33
|
+
})
|
|
34
|
+
);
|
|
26
35
|
}
|
|
27
36
|
}
|
|
28
37
|
await Promise.all(promises);
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
+
import {ChainForkConfig} from "@lodestar/config";
|
|
1
2
|
import {DataAvailabilityStatus} from "@lodestar/state-transition";
|
|
2
3
|
import {gloas} from "@lodestar/types";
|
|
4
|
+
import {TimeoutError} from "@lodestar/utils";
|
|
3
5
|
import {PayloadEnvelopeInput} from "../seenCache/seenPayloadEnvelopeInput.js";
|
|
4
|
-
|
|
5
|
-
// we can now wait for full 12 seconds because sync and reconstruction will try pulling
|
|
6
|
-
// the data columns from the network anyway while the envelope is being processed
|
|
7
|
-
export const PAYLOAD_DATA_AVAILABILITY_TIMEOUT = 12_000;
|
|
6
|
+
import {PayloadError, PayloadErrorCode} from "./importExecutionPayload.js";
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Verifies that all payload envelope inputs have their data columns available.
|
|
11
|
-
* - Waits a max of
|
|
10
|
+
* - Waits a max of one slot duration of the payload's fork for all data to be available
|
|
12
11
|
* - Returns the time at which all data was available
|
|
13
12
|
* - Returns the data availability status for each payload input
|
|
14
13
|
*/
|
|
15
14
|
export async function verifyPayloadsDataAvailability(
|
|
15
|
+
config: ChainForkConfig,
|
|
16
16
|
payloadInputs: PayloadEnvelopeInput[],
|
|
17
17
|
signal: AbortSignal
|
|
18
18
|
): Promise<{
|
|
@@ -22,7 +22,17 @@ export async function verifyPayloadsDataAvailability(
|
|
|
22
22
|
const promises: Promise<gloas.DataColumnSidecar[]>[] = [];
|
|
23
23
|
for (const payloadInput of payloadInputs) {
|
|
24
24
|
if (!payloadInput.hasAllData()) {
|
|
25
|
-
|
|
25
|
+
// Sync and reconstruction keep pulling the data columns from the network while we wait. The wait is
|
|
26
|
+
// capped at one slot because the payload queue is serial: a longer wait lets consecutive unavailable
|
|
27
|
+
// payloads push every later payload import further behind with each slot.
|
|
28
|
+
promises.push(
|
|
29
|
+
payloadInput.waitForAllData(config.getSlotDurationMs(payloadInput.forkName), signal).catch((e) => {
|
|
30
|
+
if (e instanceof TimeoutError) {
|
|
31
|
+
throw new PayloadError(payloadInput, {code: PayloadErrorCode.DATA_UNAVAILABLE});
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
})
|
|
35
|
+
);
|
|
26
36
|
}
|
|
27
37
|
}
|
|
28
38
|
await Promise.all(promises);
|
|
@@ -72,7 +72,7 @@ export enum BlockErrorCode {
|
|
|
72
72
|
* whoever served the block MAY be downscored. Mirrors PayloadErrorCode.EXECUTION_ENGINE_INVALID.
|
|
73
73
|
*/
|
|
74
74
|
EXECUTION_ENGINE_INVALID = "BLOCK_ERROR_EXECUTION_INVALID",
|
|
75
|
-
/** The
|
|
75
|
+
/** The blob or column data did not arrive within the availability wait */
|
|
76
76
|
DATA_UNAVAILABLE = "BLOCK_ERROR_DATA_UNAVAILABLE",
|
|
77
77
|
/** Block contains too many kzg commitments */
|
|
78
78
|
TOO_MANY_KZG_COMMITMENTS = "BLOCK_ERROR_TOO_MANY_KZG_COMMITMENTS",
|
package/src/chain/options.ts
CHANGED
|
@@ -48,7 +48,6 @@ export type IChainOptions = BlockProcessOpts &
|
|
|
48
48
|
/** Max number of produced block roots (blinded or full) cached for broadcast validations */
|
|
49
49
|
maxCachedProducedRoots?: number;
|
|
50
50
|
initialCustodyGroupCount?: number;
|
|
51
|
-
broadcastValidationStrictness?: string;
|
|
52
51
|
minSameMessageSignatureSetsToBatch: number;
|
|
53
52
|
archiveDateEpochs?: number;
|
|
54
53
|
nHistoricalStatesFileDataStore?: boolean;
|
|
@@ -121,7 +120,6 @@ export const defaultChainOptions: IChainOptions = {
|
|
|
121
120
|
// for attestation validation, having this value ensures we don't have to regen states most of the time
|
|
122
121
|
maxSkipSlots: 32,
|
|
123
122
|
disableProposerSlashings: false,
|
|
124
|
-
broadcastValidationStrictness: "warn",
|
|
125
123
|
// should be less than or equal to MIN_SIGNATURE_SETS_TO_BATCH_VERIFY
|
|
126
124
|
// batching too much may block the I/O thread so if useWorker=false, suggest this value to be 32
|
|
127
125
|
// since this batch attestation work is designed to work with useWorker=true, make this the lowest value
|
package/src/db/beacon.ts
CHANGED
|
@@ -30,6 +30,9 @@ import {
|
|
|
30
30
|
VoluntaryExitRepository,
|
|
31
31
|
} from "./repositories/index.js";
|
|
32
32
|
|
|
33
|
+
/** Below this size, seeks over leftover legacy column data stay cheap */
|
|
34
|
+
const LEGACY_DATA_COLUMNS_COMPACTION_MIN_BYTES = 1024 ** 3;
|
|
35
|
+
|
|
33
36
|
export type BeaconDbOpts = {
|
|
34
37
|
dataColumnDir: string;
|
|
35
38
|
logger: Logger;
|
|
@@ -67,6 +70,7 @@ export class BeaconDb implements IBeaconDb {
|
|
|
67
70
|
readonly dataColumns: IDataColumnStore;
|
|
68
71
|
lastLegacyArchiveSlot: Slot | null = null;
|
|
69
72
|
private readonly flatFileStore: FlatFileStore;
|
|
73
|
+
private readonly logger: Logger;
|
|
70
74
|
|
|
71
75
|
constructor(
|
|
72
76
|
config: ChainForkConfig,
|
|
@@ -100,6 +104,7 @@ export class BeaconDb implements IBeaconDb {
|
|
|
100
104
|
this.syncCommittee = new SyncCommitteeRepository(config, db);
|
|
101
105
|
this.syncCommitteeWitness = new SyncCommitteeWitnessRepository(config, db);
|
|
102
106
|
|
|
107
|
+
this.logger = opts.logger;
|
|
103
108
|
this.flatFileStore = new FlatFileStore(opts.dataColumnDir, config, opts.logger, opts.metrics);
|
|
104
109
|
this.dataColumns = new LegacyDataColumnStore(
|
|
105
110
|
this.flatFileStore,
|
|
@@ -125,6 +130,40 @@ export class BeaconDb implements IBeaconDb {
|
|
|
125
130
|
this.flatFileStore.setMetrics(flatFileStoreMetrics);
|
|
126
131
|
}
|
|
127
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Deletes made while an iterator held a snapshot can stay in the bottom LevelDB level, which `compactRange` never
|
|
135
|
+
* rewrites, so every seek into the legacy column buckets walks them. Writing a key at each end of the pruned range
|
|
136
|
+
* makes the compaction overlap those files and rewrite them.
|
|
137
|
+
*/
|
|
138
|
+
async compactLegacyDataColumns(
|
|
139
|
+
prunedBeforeSlot: Slot,
|
|
140
|
+
minSizeBytes = LEGACY_DATA_COLUMNS_COMPACTION_MIN_BYTES
|
|
141
|
+
): Promise<void> {
|
|
142
|
+
const archiveBucket = Bucket.allForks_dataColumnSidecarsArchive;
|
|
143
|
+
const start = encodeKey(Bucket.allForks_dataColumnSidecars, Buffer.alloc(0));
|
|
144
|
+
const end = encodeKey(archiveBucket, this.dataColumnSidecarArchive.getMinKeyRaw(prunedBeforeSlot));
|
|
145
|
+
const sizeBytes = await this.db.approximateSize(start, end);
|
|
146
|
+
if (sizeBytes < minSizeBytes) return;
|
|
147
|
+
|
|
148
|
+
// Column indices stay below 0xffff and hot column keys are shorter, so no sentinel matches a real key
|
|
149
|
+
const upperSentinel =
|
|
150
|
+
prunedBeforeSlot > 0
|
|
151
|
+
? encodeKey(archiveBucket, this.dataColumnSidecarArchive.encodeKeyRaw(prunedBeforeSlot - 1, 0xffff))
|
|
152
|
+
: encodeKey(Bucket.allForks_dataColumnSidecars, Buffer.alloc(35, 0xff));
|
|
153
|
+
const sentinels = [start, upperSentinel];
|
|
154
|
+
|
|
155
|
+
this.logger.info("Compacting legacy data columns, this may take a while", {sizeBytes});
|
|
156
|
+
const startTime = Date.now();
|
|
157
|
+
await this.db.batchPut(sentinels.map((key) => ({key, value: new Uint8Array()})));
|
|
158
|
+
await this.db.compactRange(start, end);
|
|
159
|
+
await this.db.batchDelete(sentinels);
|
|
160
|
+
await this.db.compactRange(start, end);
|
|
161
|
+
this.logger.verbose("Compacted legacy data columns", {
|
|
162
|
+
durationMs: Date.now() - startTime,
|
|
163
|
+
sizeBytes: await this.db.approximateSize(start, end),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
128
167
|
async pruneHotDb(): Promise<void> {
|
|
129
168
|
// Prune all hot blobs
|
|
130
169
|
await this.blobSidecars.batchDelete(await this.blobSidecars.keys());
|
|
@@ -13,7 +13,7 @@ export type IndexedDataColumnBytes = {index: ColumnIndex; data: Uint8Array};
|
|
|
13
13
|
type LegacyHotDataColumnStore = Pick<DataColumnSidecarRepository, "values" | "getManyBinary" | "deleteMany">;
|
|
14
14
|
type LegacyArchiveDataColumnStore = Pick<
|
|
15
15
|
DataColumnSidecarArchiveRepository,
|
|
16
|
-
"values" | "getManyBinary" | "
|
|
16
|
+
"values" | "getManyBinary" | "pruneBefore"
|
|
17
17
|
>;
|
|
18
18
|
type BlockRootIndex = Pick<BlockArchiveRepository, "getSlotByRoot">;
|
|
19
19
|
|
|
@@ -86,9 +86,7 @@ export class LegacyDataColumnStore implements IDataColumnStore {
|
|
|
86
86
|
|
|
87
87
|
async pruneBefore(slot: Slot): Promise<Slot[]> {
|
|
88
88
|
const flatFileSlots = await this.flatFiles.pruneBefore(slot);
|
|
89
|
-
const
|
|
90
|
-
const slots = [...new Set(prefixedKeys.map(({prefix}) => prefix))];
|
|
91
|
-
if (slots.length > 0) await this.legacyArchive.deleteMany(slots);
|
|
89
|
+
const slots = await this.legacyArchive.pruneBefore(slot);
|
|
92
90
|
return [...new Set([...flatFileSlots, ...slots])].sort((a, b) => a - b);
|
|
93
91
|
}
|
|
94
92
|
}
|
package/src/db/interface.ts
CHANGED
|
@@ -68,6 +68,8 @@ export interface IBeaconDb {
|
|
|
68
68
|
readonly lastLegacyArchiveSlot: Slot | null;
|
|
69
69
|
init(): Promise<void>;
|
|
70
70
|
|
|
71
|
+
compactLegacyDataColumns(prunedBeforeSlot: Slot, minSizeBytes?: number): Promise<void>;
|
|
72
|
+
|
|
71
73
|
pruneHotDb(): Promise<void>;
|
|
72
74
|
|
|
73
75
|
deleteDeprecatedEth1Data(): Promise<void>;
|