@lodestar/beacon-node 1.48.0 → 1.49.0-dev.09a1892a85
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/archiveStore/utils/pruneHistory.d.ts.map +1 -1
- package/lib/chain/archiveStore/utils/pruneHistory.js +4 -1
- package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
- package/lib/chain/errors/blockError.d.ts +0 -7
- package/lib/chain/errors/blockError.d.ts.map +1 -1
- package/lib/chain/errors/blockError.js +0 -2
- package/lib/chain/errors/blockError.js.map +1 -1
- package/lib/chain/prepareNextSlot.d.ts.map +1 -1
- package/lib/chain/prepareNextSlot.js +3 -1
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.d.ts +4 -2
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +11 -8
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/validation/block.d.ts.map +1 -1
- package/lib/chain/validation/block.js +0 -9
- package/lib/chain/validation/block.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +15 -4
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +28 -1
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/gossip/encoding.d.ts +1 -2
- package/lib/network/gossip/encoding.d.ts.map +1 -1
- package/lib/network/gossip/encoding.js +5 -10
- package/lib/network/gossip/encoding.js.map +1 -1
- package/lib/network/gossip/gossipsub.js +1 -1
- package/lib/network/gossip/gossipsub.js.map +1 -1
- package/lib/network/gossip/topic.d.ts +9 -6
- package/lib/network/gossip/topic.d.ts.map +1 -1
- package/lib/network/gossip/topic.js +23 -8
- package/lib/network/gossip/topic.js.map +1 -1
- package/lib/network/libp2p/index.d.ts +9 -1
- package/lib/network/libp2p/index.d.ts.map +1 -1
- package/lib/network/libp2p/index.js +23 -0
- package/lib/network/libp2p/index.js.map +1 -1
- package/lib/network/reqresp/protocols.d.ts.map +1 -1
- package/lib/network/reqresp/protocols.js +19 -5
- package/lib/network/reqresp/protocols.js.map +1 -1
- package/lib/sync/types.d.ts +18 -0
- package/lib/sync/types.d.ts.map +1 -1
- package/lib/sync/types.js +20 -0
- package/lib/sync/types.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +33 -12
- package/lib/sync/unknownBlock.js.map +1 -1
- package/lib/util/sszBytes.d.ts +5 -0
- package/lib/util/sszBytes.d.ts.map +1 -1
- package/lib/util/sszBytes.js +13 -1
- package/lib/util/sszBytes.js.map +1 -1
- package/package.json +16 -16
- package/src/chain/archiveStore/utils/pruneHistory.ts +4 -1
- package/src/chain/errors/blockError.ts +0 -3
- package/src/chain/prepareNextSlot.ts +5 -1
- package/src/chain/produceBlock/produceBlockBody.ts +12 -8
- package/src/chain/validation/block.ts +0 -10
- package/src/metrics/metrics/lodestar.ts +30 -3
- package/src/network/gossip/encoding.ts +5 -8
- package/src/network/gossip/gossipsub.ts +1 -1
- package/src/network/gossip/topic.ts +25 -7
- package/src/network/libp2p/index.ts +28 -1
- package/src/network/reqresp/protocols.ts +31 -6
- package/src/sync/types.ts +20 -0
- package/src/sync/unknownBlock.ts +47 -12
- package/src/util/sszBytes.ts +18 -0
|
@@ -188,16 +188,6 @@ export async function validateGossipBlock(
|
|
|
188
188
|
});
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
// [REJECT] The block contains no deposits.
|
|
192
|
-
if (body.deposits.length !== 0) {
|
|
193
|
-
throw new BlockGossipError(GossipAction.REJECT, {
|
|
194
|
-
code: BlockErrorCode.NON_ZERO_DEPOSITS,
|
|
195
|
-
slot: blockSlot,
|
|
196
|
-
root: blockRoot,
|
|
197
|
-
count: body.deposits.length,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
|
|
201
191
|
// TODO GLOAS: [REJECT] The block's execution payload parent (defined by bid.parent_block_hash) passes all validation
|
|
202
192
|
// This requires execution engine integration to verify the parent block hash
|
|
203
193
|
}
|
|
@@ -26,7 +26,7 @@ import {ExecutionPayloadStatus} from "../../execution/index.js";
|
|
|
26
26
|
import {GossipType} from "../../network/index.js";
|
|
27
27
|
import {CannotAcceptWorkReason, ReprocessRejectReason} from "../../network/processor/index.js";
|
|
28
28
|
import {BackfillSyncMethod} from "../../sync/backfill/backfill.js";
|
|
29
|
-
import {DroppedItemReason, PendingBlockType} from "../../sync/types.js";
|
|
29
|
+
import {DownloadResult, DroppedItemReason, FetchResult, PendingBlockType} from "../../sync/types.js";
|
|
30
30
|
import {PeerSyncType, RangeSyncType} from "../../sync/utils/remoteSyncType.js";
|
|
31
31
|
import {AllocSource} from "../../util/bufferPool.js";
|
|
32
32
|
import {DataColumnReconstructionCode} from "../../util/dataColumns.js";
|
|
@@ -709,17 +709,44 @@ export function createLodestarMetrics(
|
|
|
709
709
|
buckets: [0, 1, 2, 4],
|
|
710
710
|
}),
|
|
711
711
|
// we may not have slot in case of failure, so track fetch time from start to done (either success or failure)
|
|
712
|
-
|
|
712
|
+
fetchTime: register.histogram<{result: FetchResult}>({
|
|
713
713
|
name: "lodestar_sync_unknown_block_fetch_time_seconds",
|
|
714
714
|
help: "Fetch time from start to done (either success or failure)",
|
|
715
715
|
labelNames: ["result"],
|
|
716
716
|
buckets: [0, 1, 2, 4, 8],
|
|
717
717
|
}),
|
|
718
|
-
fetchPeers: register.gauge<{result:
|
|
718
|
+
fetchPeers: register.gauge<{result: FetchResult}>({
|
|
719
719
|
name: "lodestar_sync_unknown_block_fetch_peers_count",
|
|
720
720
|
help: "Number of peers that node fetched from",
|
|
721
721
|
labelNames: ["result"],
|
|
722
722
|
}),
|
|
723
|
+
downloadedPayloadsResult: register.counter<{result: DownloadResult}>({
|
|
724
|
+
name: "lodestar_sync_unknown_payload_download_result_total",
|
|
725
|
+
help: "Total number of downloadPayload results in UnknownBlockSync",
|
|
726
|
+
labelNames: ["result"],
|
|
727
|
+
}),
|
|
728
|
+
elapsedTimeTillPayloadReceived: register.histogram({
|
|
729
|
+
name: "lodestar_sync_unknown_payload_elapsed_time_till_received_seconds",
|
|
730
|
+
help: "Time elapsed between payload slot time and the time payload received via unknown block sync",
|
|
731
|
+
buckets: [6, 8, 10, 12],
|
|
732
|
+
}),
|
|
733
|
+
payloadFetchBegin: register.histogram({
|
|
734
|
+
name: "lodestar_sync_unknown_payload_fetch_begin_since_slot_start_seconds",
|
|
735
|
+
help: "Time into the slot when the payload was fetched",
|
|
736
|
+
buckets: [6, 8, 10, 12],
|
|
737
|
+
}),
|
|
738
|
+
// we may not have slot in case of failure, so track fetch time from start to done (either success or failure)
|
|
739
|
+
payloadFetchTime: register.histogram<{result: FetchResult}>({
|
|
740
|
+
name: "lodestar_sync_unknown_payload_fetch_time_seconds",
|
|
741
|
+
help: "Payload fetch time from start to done (either success or failure)",
|
|
742
|
+
labelNames: ["result"],
|
|
743
|
+
buckets: [0, 1, 2, 4, 8],
|
|
744
|
+
}),
|
|
745
|
+
payloadFetchPeers: register.gauge<{result: FetchResult}>({
|
|
746
|
+
name: "lodestar_sync_unknown_payload_fetch_peers_count",
|
|
747
|
+
help: "Number of peers that node fetched the payload from",
|
|
748
|
+
labelNames: ["result"],
|
|
749
|
+
}),
|
|
723
750
|
downloadByRoot: {
|
|
724
751
|
success: register.gauge({
|
|
725
752
|
name: "lodestar_sync_unknown_block_download_by_root_success_total",
|
|
@@ -8,7 +8,7 @@ import {ForkName} from "@lodestar/params";
|
|
|
8
8
|
import {intToBytes} from "@lodestar/utils";
|
|
9
9
|
import {MESSAGE_DOMAIN_VALID_SNAPPY} from "./constants.js";
|
|
10
10
|
import {Eth2GossipsubMetrics} from "./metrics.js";
|
|
11
|
-
import {GossipTopicCache
|
|
11
|
+
import {GossipTopicCache} from "./topic.js";
|
|
12
12
|
|
|
13
13
|
// Load WASM
|
|
14
14
|
const xxhash = await xxhashFactory();
|
|
@@ -97,7 +97,6 @@ export function msgIdFn(gossipTopicCache: GossipTopicCache, msg: Message): Uint8
|
|
|
97
97
|
export class DataTransformSnappy implements DataTransform {
|
|
98
98
|
constructor(
|
|
99
99
|
private readonly gossipTopicCache: GossipTopicCache,
|
|
100
|
-
private readonly maxPayloadSize: number,
|
|
101
100
|
private readonly metrics: Eth2GossipsubMetrics | null
|
|
102
101
|
) {}
|
|
103
102
|
|
|
@@ -112,12 +111,11 @@ export class DataTransformSnappy implements DataTransform {
|
|
|
112
111
|
const uncompressedDataLength = snappyWasm.decompress_len(data);
|
|
113
112
|
|
|
114
113
|
const topic = this.gossipTopicCache.getTopic(topicStr);
|
|
115
|
-
const
|
|
116
|
-
const maxSize = getGossipSSZMaxSize(topic, this.maxPayloadSize, sszType);
|
|
114
|
+
const {minSize, maxSize} = this.gossipTopicCache.getTypeSizes(topicStr);
|
|
117
115
|
this.metrics?.dataTransform.inbound.inc({type: topic.type});
|
|
118
116
|
|
|
119
|
-
if (uncompressedDataLength <
|
|
120
|
-
throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} < ${
|
|
117
|
+
if (uncompressedDataLength < minSize) {
|
|
118
|
+
throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} < ${minSize}`);
|
|
121
119
|
}
|
|
122
120
|
if (uncompressedDataLength > maxSize) {
|
|
123
121
|
throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${maxSize}`);
|
|
@@ -136,8 +134,7 @@ export class DataTransformSnappy implements DataTransform {
|
|
|
136
134
|
*/
|
|
137
135
|
outboundTransform(topicStr: string, data: Uint8Array): Uint8Array {
|
|
138
136
|
const topic = this.gossipTopicCache.getTopic(topicStr);
|
|
139
|
-
const
|
|
140
|
-
const maxSize = getGossipSSZMaxSize(topic, this.maxPayloadSize, sszType);
|
|
137
|
+
const {maxSize} = this.gossipTopicCache.getTypeSizes(topicStr);
|
|
141
138
|
this.metrics?.dataTransform.outbound.inc({type: topic.type});
|
|
142
139
|
if (data.length > maxSize) {
|
|
143
140
|
throw Error(`ssz_snappy encoded data length ${data.length} > ${maxSize}`);
|
|
@@ -168,7 +168,7 @@ export class Eth2Gossipsub {
|
|
|
168
168
|
fastMsgIdFn: fastMsgIdFn,
|
|
169
169
|
msgIdFn: msgIdFn.bind(msgIdFn, gossipTopicCache),
|
|
170
170
|
msgIdToStrFn: msgIdToStrFn,
|
|
171
|
-
dataTransform: new DataTransformSnappy(gossipTopicCache,
|
|
171
|
+
dataTransform: new DataTransformSnappy(gossipTopicCache, metrics),
|
|
172
172
|
metricsRegister: metricsRegister as MetricsRegister | null,
|
|
173
173
|
metricsTopicStrToLabel: metricsRegister
|
|
174
174
|
? getMetricsTopicStrToLabel(networkConfig, {disableLightClientServer: opts.disableLightClientServer ?? false})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {type CompositeTypeAny} from "@chainsafe/ssz";
|
|
2
|
-
import {ForkDigestContext} from "@lodestar/config";
|
|
2
|
+
import {BeaconConfig, ChainForkConfig, ForkDigestContext} from "@lodestar/config";
|
|
3
3
|
import {
|
|
4
4
|
ATTESTATION_SUBNET_COUNT,
|
|
5
5
|
ForkName,
|
|
@@ -10,32 +10,47 @@ import {
|
|
|
10
10
|
isForkPostFulu,
|
|
11
11
|
isForkPostGloas,
|
|
12
12
|
} from "@lodestar/params";
|
|
13
|
+
import {TypeSizes} from "@lodestar/reqresp";
|
|
13
14
|
import {Attestation, SingleAttestation, ssz, sszTypesFor} from "@lodestar/types";
|
|
14
15
|
import {GossipAction, GossipActionError, GossipErrorCode} from "../../chain/errors/gossipValidation.js";
|
|
16
|
+
import {computeMaxGloasDataColumnSidecarSize} from "../../util/sszBytes.js";
|
|
15
17
|
import {NetworkConfig} from "../networkConfig.js";
|
|
16
18
|
import {DEFAULT_ENCODING} from "./constants.js";
|
|
17
19
|
import {GossipEncoding, GossipTopic, GossipTopicTypeMap, GossipType, SSZTypeOfGossipTopic} from "./interface.js";
|
|
18
20
|
|
|
19
21
|
export interface IGossipTopicCache {
|
|
20
22
|
getTopic(topicStr: string): GossipTopic;
|
|
23
|
+
getTypeSizes(topicStr: string): TypeSizes;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export class GossipTopicCache implements IGossipTopicCache {
|
|
24
27
|
private topicsByTopicStr = new Map<string, Required<GossipTopic>>();
|
|
28
|
+
private typeSizesByTopicStr = new Map<string, TypeSizes>();
|
|
25
29
|
|
|
26
|
-
constructor(private readonly
|
|
30
|
+
constructor(private readonly config: BeaconConfig) {}
|
|
27
31
|
|
|
28
32
|
/** Returns cached GossipTopic, otherwise attempts to parse it from the str */
|
|
29
33
|
getTopic(topicStr: string): GossipTopic {
|
|
30
34
|
let topic = this.topicsByTopicStr.get(topicStr);
|
|
31
35
|
if (topic === undefined) {
|
|
32
|
-
topic = parseGossipTopic(this.
|
|
36
|
+
topic = parseGossipTopic(this.config, topicStr);
|
|
33
37
|
// TODO: Consider just throwing here. We should only receive messages from known subscribed topics
|
|
34
38
|
this.topicsByTopicStr.set(topicStr, topic);
|
|
35
39
|
}
|
|
36
40
|
return topic;
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
getTypeSizes(topicStr: string): TypeSizes {
|
|
44
|
+
let typeSizes = this.typeSizesByTopicStr.get(topicStr);
|
|
45
|
+
if (typeSizes === undefined) {
|
|
46
|
+
const topic = this.getTopic(topicStr);
|
|
47
|
+
const sszType = getGossipSSZType(topic);
|
|
48
|
+
typeSizes = {minSize: sszType.minSize, maxSize: getGossipSSZMaxSize(topic, this.config, sszType)};
|
|
49
|
+
this.typeSizesByTopicStr.set(topicStr, typeSizes);
|
|
50
|
+
}
|
|
51
|
+
return typeSizes;
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
/** Returns cached GossipTopic, otherwise returns undefined */
|
|
40
55
|
getKnownTopic(topicStr: string): GossipTopic | undefined {
|
|
41
56
|
return this.topicsByTopicStr.get(topicStr);
|
|
@@ -133,11 +148,14 @@ export function getGossipSSZType(topic: GossipTopic) {
|
|
|
133
148
|
}
|
|
134
149
|
|
|
135
150
|
/**
|
|
136
|
-
* Return the maximum uncompressed SSZ byte length
|
|
137
|
-
* or MAX_PAYLOAD_SIZE, whichever is smaller.
|
|
151
|
+
* Return the maximum uncompressed SSZ byte length allowed by the type and configured network bounds.
|
|
138
152
|
*/
|
|
139
|
-
export function getGossipSSZMaxSize(topic: GossipTopic,
|
|
140
|
-
|
|
153
|
+
export function getGossipSSZMaxSize(topic: GossipTopic, config: ChainForkConfig, sszType?: CompositeTypeAny): number {
|
|
154
|
+
const maxSize = Math.min((sszType ?? getGossipSSZType(topic)).maxSize, config.MAX_PAYLOAD_SIZE);
|
|
155
|
+
if (isForkPostGloas(topic.boundary.fork) && topic.type === GossipType.data_column_sidecar) {
|
|
156
|
+
return Math.min(maxSize, computeMaxGloasDataColumnSidecarSize(config));
|
|
157
|
+
}
|
|
158
|
+
return maxSize;
|
|
141
159
|
}
|
|
142
160
|
|
|
143
161
|
/**
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {getCiphers} from "node:crypto";
|
|
2
2
|
import {bootstrap} from "@libp2p/bootstrap";
|
|
3
3
|
import {identify} from "@libp2p/identify";
|
|
4
|
-
import type {PrivateKey} from "@libp2p/interface";
|
|
4
|
+
import type {Address, PrivateKey} from "@libp2p/interface";
|
|
5
5
|
import {mdns} from "@libp2p/mdns";
|
|
6
6
|
import {mplex} from "@libp2p/mplex";
|
|
7
7
|
import {prometheusMetrics} from "@libp2p/prometheus-metrics";
|
|
8
8
|
import {tcp} from "@libp2p/tcp";
|
|
9
|
+
import {loopbackAddressLast, publicAddressesFirst, reliableTransportsFirst} from "@libp2p/utils";
|
|
10
|
+
import type {Multiaddr} from "@multiformats/multiaddr";
|
|
9
11
|
import {Libp2pInit, createLibp2p} from "libp2p";
|
|
10
12
|
import {Registry} from "prom-client";
|
|
11
13
|
import {ENR} from "@chainsafe/enr";
|
|
@@ -24,6 +26,29 @@ const noiseCrypto = getCiphers().includes("chacha20-poly1305")
|
|
|
24
26
|
chaCha20Poly1305Decrypt: asCrypto.chaCha20Poly1305Decrypt,
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
function quicAddressesFirst(a: Multiaddr, b: Multiaddr): -1 | 0 | 1 {
|
|
30
|
+
const aQuic = a.getComponents().some((c) => c.name === "quic-v1");
|
|
31
|
+
const bQuic = b.getComponents().some((c) => c.name === "quic-v1");
|
|
32
|
+
if (aQuic === bQuic) return 0;
|
|
33
|
+
return aQuic ? -1 : 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Custom sorter for libp2p's `addressSorter`.
|
|
38
|
+
*
|
|
39
|
+
* Prefers QUIC > TCP addresses before dialing, per p2p spec.
|
|
40
|
+
*
|
|
41
|
+
* Spec: https://github.com/ethereum/consensus-specs/blob/f21dac06e99743b1e98bb80297897b96520c942b/specs/phase0/p2p-interface.md#transport
|
|
42
|
+
*/
|
|
43
|
+
export function quicFirstAddressSorter(a: Address, b: Address): -1 | 0 | 1 {
|
|
44
|
+
return (
|
|
45
|
+
loopbackAddressLast(a.multiaddr, b.multiaddr) ||
|
|
46
|
+
publicAddressesFirst(a.multiaddr, b.multiaddr) ||
|
|
47
|
+
quicAddressesFirst(a.multiaddr, b.multiaddr) ||
|
|
48
|
+
reliableTransportsFirst(a.multiaddr, b.multiaddr)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
27
52
|
export type NodeJsLibp2pOpts = {
|
|
28
53
|
peerStoreDir?: string;
|
|
29
54
|
disablePeerDiscovery?: boolean;
|
|
@@ -150,6 +175,8 @@ export async function createNodeJsLibp2p(
|
|
|
150
175
|
// the maximum number of pending connections libp2p will accept before it starts rejecting incoming connections.
|
|
151
176
|
// make it the same to backlog option above
|
|
152
177
|
maxIncomingPendingConnections: 5,
|
|
178
|
+
// Prefer dialing QUIC > TCP for a peer address if QUIC is enabled
|
|
179
|
+
addressSorter: quicEnabled ? quicFirstAddressSorter : undefined,
|
|
153
180
|
},
|
|
154
181
|
// rely on lodestar's peer manager to ping peers
|
|
155
182
|
connectionMonitor: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {BeaconConfig} from "@lodestar/config";
|
|
2
|
-
import {ForkName} from "@lodestar/params";
|
|
2
|
+
import {ForkName, isForkPostGloas} from "@lodestar/params";
|
|
3
3
|
import {ContextBytesFactory, ContextBytesType, Encoding, TypeSizes} from "@lodestar/reqresp";
|
|
4
|
+
import {computeMaxGloasDataColumnSidecarSize} from "../../util/sszBytes.js";
|
|
4
5
|
import {rateLimitQuotas} from "./rateLimit.js";
|
|
5
6
|
import {ProtocolNoHandler, ReqRespMethod, Version, requestSszTypeByMethod, responseSszTypeByMethod} from "./types.js";
|
|
6
7
|
|
|
@@ -151,18 +152,42 @@ function toProtocol(protocol: ProtocolSummary) {
|
|
|
151
152
|
encoding: Encoding.SSZ_SNAPPY,
|
|
152
153
|
contextBytes: toContextBytes(protocol.contextBytesType, config),
|
|
153
154
|
inboundRateLimits: rateLimitQuotas(fork, config)[protocol.method],
|
|
154
|
-
requestSizes: requestType === null ? null : clampTypeSizes(requestType, config),
|
|
155
|
-
responseSizes: (fork) =>
|
|
155
|
+
requestSizes: requestType === null ? null : clampTypeSizes(requestType, config.MAX_PAYLOAD_SIZE),
|
|
156
|
+
responseSizes: (fork) =>
|
|
157
|
+
clampResponseTypeSizes(
|
|
158
|
+
responseSszTypeByMethod[protocol.method](fork, protocol.version),
|
|
159
|
+
protocol.method,
|
|
160
|
+
fork,
|
|
161
|
+
config
|
|
162
|
+
),
|
|
156
163
|
};
|
|
157
164
|
};
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
function clampResponseTypeSizes(
|
|
168
|
+
type: TypeSizes,
|
|
169
|
+
method: ReqRespMethod,
|
|
170
|
+
fork: ForkName,
|
|
171
|
+
config: BeaconConfig
|
|
172
|
+
): TypeSizes {
|
|
173
|
+
let maxPayloadSize = config.MAX_PAYLOAD_SIZE;
|
|
174
|
+
if (isForkPostGloas(fork)) {
|
|
175
|
+
switch (method) {
|
|
176
|
+
case ReqRespMethod.DataColumnSidecarsByRange:
|
|
177
|
+
case ReqRespMethod.DataColumnSidecarsByRoot:
|
|
178
|
+
maxPayloadSize = Math.min(maxPayloadSize, computeMaxGloasDataColumnSidecarSize(config));
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return clampTypeSizes(type, maxPayloadSize);
|
|
183
|
+
}
|
|
184
|
+
|
|
160
185
|
/**
|
|
161
|
-
* Length-prefix must be within the SSZ type bounds or
|
|
186
|
+
* Length-prefix must be within the SSZ type bounds or the configured payload limit, whichever is smaller.
|
|
162
187
|
* https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/p2p-interface.md#encoding-strategies
|
|
163
188
|
*/
|
|
164
|
-
function clampTypeSizes(type: TypeSizes,
|
|
165
|
-
return {minSize: type.minSize, maxSize: Math.min(type.maxSize,
|
|
189
|
+
function clampTypeSizes(type: TypeSizes, maxPayloadSize: number): TypeSizes {
|
|
190
|
+
return {minSize: type.minSize, maxSize: Math.min(type.maxSize, maxPayloadSize)};
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
function toContextBytes(type: ContextBytesType, config: BeaconConfig): ContextBytesFactory {
|
package/src/sync/types.ts
CHANGED
|
@@ -29,6 +29,26 @@ export enum PendingBlockType {
|
|
|
29
29
|
INCOMPLETE_PAYLOAD_ENVELOPE = "incomplete_payload_envelope",
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export enum DownloadResult {
|
|
33
|
+
/** payload fully downloaded, ready to process */
|
|
34
|
+
Resolved = "resolved",
|
|
35
|
+
/** payload fully downloaded, but it was already imported (via gossip) while we were downloading */
|
|
36
|
+
Late = "late",
|
|
37
|
+
/** envelope downloaded, held until the block lands in fork choice */
|
|
38
|
+
WaitingForBlock = "waiting_for_block",
|
|
39
|
+
/** fetch failed, the pending entry is retained for retry */
|
|
40
|
+
Failed = "failed",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export enum FetchResult {
|
|
44
|
+
SuccessResolved = "success_resolved",
|
|
45
|
+
SuccessMissingParent = "success_missing_parent",
|
|
46
|
+
SuccessLate = "success_late",
|
|
47
|
+
SuccessWaitingForBlock = "success_waiting_for_block",
|
|
48
|
+
FailureTriedAllPeers = "failure_tried_all_peers",
|
|
49
|
+
FailureMaxAttempts = "failure_max_attempts",
|
|
50
|
+
}
|
|
51
|
+
|
|
32
52
|
export enum PendingBlockInputStatus {
|
|
33
53
|
pending = "pending",
|
|
34
54
|
fetching = "fetching",
|
package/src/sync/unknownBlock.ts
CHANGED
|
@@ -26,7 +26,9 @@ import {MAX_CONCURRENT_REQUESTS} from "./constants.js";
|
|
|
26
26
|
import {SyncOptions} from "./options.js";
|
|
27
27
|
import {
|
|
28
28
|
BlockInputSyncCacheItem,
|
|
29
|
+
DownloadResult,
|
|
29
30
|
DroppedItemReason,
|
|
31
|
+
FetchResult,
|
|
30
32
|
PayloadSyncCacheItem,
|
|
31
33
|
PendingBlockInput,
|
|
32
34
|
PendingBlockInputStatus,
|
|
@@ -63,14 +65,6 @@ type AdvancePendingBlockResult =
|
|
|
63
65
|
| "blocked"
|
|
64
66
|
| "removed";
|
|
65
67
|
|
|
66
|
-
enum FetchResult {
|
|
67
|
-
SuccessResolved = "success_resolved",
|
|
68
|
-
SuccessMissingParent = "success_missing_parent",
|
|
69
|
-
SuccessLate = "success_late",
|
|
70
|
-
FailureTriedAllPeers = "failure_tried_all_peers",
|
|
71
|
-
FailureMaxAttempts = "failure_max_attempts",
|
|
72
|
-
}
|
|
73
|
-
|
|
74
68
|
class UnknownBlockRateLimitedError extends Error {
|
|
75
69
|
constructor(message: string) {
|
|
76
70
|
super(message);
|
|
@@ -1125,15 +1119,25 @@ export class BlockInputSync {
|
|
|
1125
1119
|
return;
|
|
1126
1120
|
}
|
|
1127
1121
|
this.pendingPayloads.set(resultRootHex, pendingPayload);
|
|
1122
|
+
const result = isPendingPayloadEnvelope(pendingPayload)
|
|
1123
|
+
? DownloadResult.WaitingForBlock
|
|
1124
|
+
: this.chain.forkChoice.hasPayloadHexUnsafe(resultRootHex)
|
|
1125
|
+
? DownloadResult.Late
|
|
1126
|
+
: DownloadResult.Resolved;
|
|
1127
|
+
this.metrics?.blockInputSync.downloadedPayloadsResult.inc({result});
|
|
1128
1128
|
|
|
1129
1129
|
if (isPendingPayloadEnvelope(pendingPayload)) {
|
|
1130
1130
|
await this.reconcilePayloadEnvelope(pendingPayload);
|
|
1131
1131
|
} else if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
|
|
1132
|
+
this.metrics?.blockInputSync.elapsedTimeTillPayloadReceived.observe(
|
|
1133
|
+
Date.now() / 1000 - computeTimeAtSlot(this.config, pendingPayload.payloadInput.slot, this.chain.genesisTime)
|
|
1134
|
+
);
|
|
1132
1135
|
await this.processPayload(pendingPayload);
|
|
1133
1136
|
}
|
|
1134
1137
|
return;
|
|
1135
1138
|
}
|
|
1136
1139
|
|
|
1140
|
+
this.metrics?.blockInputSync.downloadedPayloadsResult.inc({result: DownloadResult.Failed});
|
|
1137
1141
|
this.logger.debug("Ignoring unknown payload root after failed download", logCtx, res.err);
|
|
1138
1142
|
if (!isPendingPayloadEnvelope(payload)) {
|
|
1139
1143
|
payload.status = PendingPayloadInputStatus.pending;
|
|
@@ -1248,6 +1252,11 @@ export class BlockInputSync {
|
|
|
1248
1252
|
: await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
|
|
1249
1253
|
let envelope = payloadInput?.hasPayloadEnvelope() ? payloadInput.getPayloadEnvelope() : undefined;
|
|
1250
1254
|
|
|
1255
|
+
const fetchStartSec = Date.now() / 1000;
|
|
1256
|
+
if (typeof slot === "number") {
|
|
1257
|
+
this.metrics?.blockInputSync.payloadFetchBegin.observe(this.chain.clock.secFromSlot(slot, fetchStartSec));
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1251
1260
|
let i = 0;
|
|
1252
1261
|
let deferredByRateLimit = false;
|
|
1253
1262
|
while (i++ < this.getMaxDownloadAttempts()) {
|
|
@@ -1271,6 +1280,11 @@ export class BlockInputSync {
|
|
|
1271
1280
|
pendingColumns.size > 0
|
|
1272
1281
|
? `cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`
|
|
1273
1282
|
: "no peer available to download pending payload";
|
|
1283
|
+
this.metrics?.blockInputSync.payloadFetchTime.observe(
|
|
1284
|
+
{result: FetchResult.FailureTriedAllPeers},
|
|
1285
|
+
Date.now() / 1000 - fetchStartSec
|
|
1286
|
+
);
|
|
1287
|
+
this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.FailureTriedAllPeers}, i);
|
|
1274
1288
|
throw Error(`Error fetching payload by root slot=${slot} root=${rootHex} after ${i}: ${reason}`);
|
|
1275
1289
|
}
|
|
1276
1290
|
|
|
@@ -1280,7 +1294,12 @@ export class BlockInputSync {
|
|
|
1280
1294
|
try {
|
|
1281
1295
|
if (!envelope) {
|
|
1282
1296
|
envelope = await this.fetchExecutionPayloadEnvelope(peerId, blockRoot, rootHex);
|
|
1297
|
+
const slotWasUnknown = typeof slot !== "number";
|
|
1283
1298
|
slot = envelope.message.payload.slotNumber;
|
|
1299
|
+
if (slotWasUnknown) {
|
|
1300
|
+
// we were not able to observe the time into slot when starting the fetch, do it now
|
|
1301
|
+
this.metrics?.blockInputSync.payloadFetchBegin.observe(this.chain.clock.secFromSlot(slot, fetchStartSec));
|
|
1302
|
+
}
|
|
1284
1303
|
}
|
|
1285
1304
|
|
|
1286
1305
|
payloadInput ??= await this.chain.seenPayloadEnvelopeInputCache.getOrReload(rootHex);
|
|
@@ -1289,6 +1308,11 @@ export class BlockInputSync {
|
|
|
1289
1308
|
// envelope and wait for the block body; reconcilePayloadEnvelope validates once the block lands.
|
|
1290
1309
|
// payloadInput may be seeded from the block body during download, so a non-null payloadInput does not
|
|
1291
1310
|
// imply the block is imported.
|
|
1311
|
+
this.metrics?.blockInputSync.payloadFetchTime.observe(
|
|
1312
|
+
{result: FetchResult.SuccessWaitingForBlock},
|
|
1313
|
+
Date.now() / 1000 - fetchStartSec
|
|
1314
|
+
);
|
|
1315
|
+
this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.SuccessWaitingForBlock}, i);
|
|
1292
1316
|
return {
|
|
1293
1317
|
status: PendingPayloadInputStatus.waitingForBlock,
|
|
1294
1318
|
envelope,
|
|
@@ -1339,6 +1363,12 @@ export class BlockInputSync {
|
|
|
1339
1363
|
});
|
|
1340
1364
|
|
|
1341
1365
|
if (pendingPayload.status === PendingPayloadInputStatus.downloaded) {
|
|
1366
|
+
// late = the payload got imported (via gossip) while we were downloading, same as the block path
|
|
1367
|
+
const result = this.chain.forkChoice.hasPayloadHexUnsafe(rootHex)
|
|
1368
|
+
? FetchResult.SuccessLate
|
|
1369
|
+
: FetchResult.SuccessResolved;
|
|
1370
|
+
this.metrics?.blockInputSync.payloadFetchTime.observe({result}, Date.now() / 1000 - fetchStartSec);
|
|
1371
|
+
this.metrics?.blockInputSync.payloadFetchPeers.set({result}, i);
|
|
1342
1372
|
return pendingPayload;
|
|
1343
1373
|
}
|
|
1344
1374
|
|
|
@@ -1373,12 +1403,17 @@ export class BlockInputSync {
|
|
|
1373
1403
|
}
|
|
1374
1404
|
}
|
|
1375
1405
|
|
|
1406
|
+
this.metrics?.blockInputSync.payloadFetchTime.observe(
|
|
1407
|
+
{result: FetchResult.FailureMaxAttempts},
|
|
1408
|
+
Date.now() / 1000 - fetchStartSec
|
|
1409
|
+
);
|
|
1410
|
+
this.metrics?.blockInputSync.payloadFetchPeers.set({result: FetchResult.FailureMaxAttempts}, i - 1);
|
|
1411
|
+
|
|
1376
1412
|
if (deferredByRateLimit && this.peerBalancer.getNextRateLimitRetryAt() !== null) {
|
|
1377
1413
|
throw new UnknownBlockRateLimitedError(
|
|
1378
1414
|
`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts: peers are rate-limited`
|
|
1379
1415
|
);
|
|
1380
1416
|
}
|
|
1381
|
-
|
|
1382
1417
|
throw Error(`Error fetching payload with slot=${slot} root=${rootHex} after ${i - 1} attempts.`);
|
|
1383
1418
|
}
|
|
1384
1419
|
|
|
@@ -1483,7 +1518,7 @@ export class BlockInputSync {
|
|
|
1483
1518
|
|
|
1484
1519
|
// no more peer with needed columns to try, throw error
|
|
1485
1520
|
const message = `Error fetching UnknownBlockRoot slot=${slot} root=${rootHex} after ${i}: cannot find peer with needed columns=${prettyPrintIndices(Array.from(pendingColumns))}`;
|
|
1486
|
-
this.metrics?.blockInputSync.
|
|
1521
|
+
this.metrics?.blockInputSync.fetchTime.observe(
|
|
1487
1522
|
{result: FetchResult.FailureTriedAllPeers},
|
|
1488
1523
|
Date.now() / 1000 - fetchStartSec
|
|
1489
1524
|
);
|
|
@@ -1572,7 +1607,7 @@ export class BlockInputSync {
|
|
|
1572
1607
|
: this.chain.forkChoice.hasBlockHex(cacheItem.blockInput.parentRootHex)
|
|
1573
1608
|
? FetchResult.SuccessResolved
|
|
1574
1609
|
: FetchResult.SuccessMissingParent;
|
|
1575
|
-
this.metrics?.blockInputSync.
|
|
1610
|
+
this.metrics?.blockInputSync.fetchTime.observe({result}, Date.now() / 1000 - fetchStartSec);
|
|
1576
1611
|
this.metrics?.blockInputSync.fetchPeers.set({result}, i);
|
|
1577
1612
|
return cacheItem;
|
|
1578
1613
|
}
|
|
@@ -1606,7 +1641,7 @@ export class BlockInputSync {
|
|
|
1606
1641
|
}
|
|
1607
1642
|
}
|
|
1608
1643
|
|
|
1609
|
-
this.metrics?.blockInputSync.
|
|
1644
|
+
this.metrics?.blockInputSync.fetchTime.observe(
|
|
1610
1645
|
{result: FetchResult.FailureMaxAttempts},
|
|
1611
1646
|
Date.now() / 1000 - fetchStartSec
|
|
1612
1647
|
);
|
package/src/util/sszBytes.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {BitArray, deserializeUint8ArrayBitListFromBytes} from "@chainsafe/ssz";
|
|
2
2
|
import {ChainForkConfig} from "@lodestar/config";
|
|
3
3
|
import {
|
|
4
|
+
BYTES_PER_CELL,
|
|
4
5
|
BYTES_PER_FIELD_ELEMENT,
|
|
5
6
|
FIELD_ELEMENTS_PER_BLOB,
|
|
6
7
|
ForkName,
|
|
@@ -551,6 +552,23 @@ export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Arr
|
|
|
551
552
|
return "0x" + blockRootBuf.toString("hex");
|
|
552
553
|
}
|
|
553
554
|
|
|
555
|
+
// beaconBlockRoot is the last fixed field in the Gloas layout
|
|
556
|
+
const DATA_COLUMN_SIDECAR_GLOAS_FIXED_SIZE = BEACON_BLOCK_ROOT_POSITION_IN_GLOAS_DATA_COLUMN_SIDECAR + ROOT_SIZE; // 56 bytes
|
|
557
|
+
const KZG_PROOF_SIZE = 48; // Bytes48
|
|
558
|
+
const DATA_COLUMN_SIDECAR_GLOAS_PER_BLOB_SIZE = BYTES_PER_CELL + KZG_PROOF_SIZE; // 2048 + 48 = 2096 bytes
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Gloas sidecar size bound uses the largest blob limit across the entire schedule, including future entries.
|
|
562
|
+
* https://github.com/ethereum/consensus-specs/pull/5613
|
|
563
|
+
*/
|
|
564
|
+
export function computeMaxGloasDataColumnSidecarSize(config: ChainForkConfig): number {
|
|
565
|
+
const maxBlobs = config.BLOB_SCHEDULE.reduce(
|
|
566
|
+
(max, entry) => Math.max(max, entry.MAX_BLOBS_PER_BLOCK),
|
|
567
|
+
config.MAX_BLOBS_PER_BLOCK_ELECTRA
|
|
568
|
+
);
|
|
569
|
+
return DATA_COLUMN_SIDECAR_GLOAS_FIXED_SIZE + maxBlobs * DATA_COLUMN_SIDECAR_GLOAS_PER_BLOB_SIZE;
|
|
570
|
+
}
|
|
571
|
+
|
|
554
572
|
/**
|
|
555
573
|
* SignedExecutionPayloadEnvelope SSZ Layout:
|
|
556
574
|
* ├─ 4 bytes: message offset (points to byte 100)
|