@lodestar/beacon-node 1.50.0-dev.ebecebb9ba → 1.50.0-dev.ece0239bd3
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/pool/index.js +4 -4
- package/lib/api/impl/beacon/pool/index.js.map +1 -1
- package/lib/chain/archiveStore/archiveStore.d.ts +1 -0
- package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
- package/lib/chain/archiveStore/archiveStore.js +6 -2
- package/lib/chain/archiveStore/archiveStore.js.map +1 -1
- package/lib/chain/archiveStore/utils/pruneHistory.d.ts +4 -1
- package/lib/chain/archiveStore/utils/pruneHistory.d.ts.map +1 -1
- package/lib/chain/archiveStore/utils/pruneHistory.js +4 -3
- package/lib/chain/archiveStore/utils/pruneHistory.js.map +1 -1
- package/lib/chain/chain.js +3 -3
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/initState.d.ts +2 -9
- package/lib/chain/initState.d.ts.map +1 -1
- package/lib/chain/initState.js +5 -38
- package/lib/chain/initState.js.map +1 -1
- package/lib/chain/opPools/opPool.d.ts +5 -4
- package/lib/chain/opPools/opPool.d.ts.map +1 -1
- package/lib/chain/opPools/opPool.js +62 -18
- package/lib/chain/opPools/opPool.js.map +1 -1
- package/lib/chain/opPools/utils.d.ts +4 -1
- package/lib/chain/opPools/utils.d.ts.map +1 -1
- package/lib/chain/opPools/utils.js +11 -1
- package/lib/chain/opPools/utils.js.map +1 -1
- package/lib/chain/validation/attesterSlashing.d.ts +4 -4
- package/lib/chain/validation/attesterSlashing.d.ts.map +1 -1
- package/lib/chain/validation/attesterSlashing.js +3 -0
- package/lib/chain/validation/attesterSlashing.js.map +1 -1
- package/lib/chain/validation/proposerSlashing.d.ts +3 -3
- package/lib/chain/validation/proposerSlashing.d.ts.map +1 -1
- package/lib/chain/validation/proposerSlashing.js +3 -0
- package/lib/chain/validation/proposerSlashing.js.map +1 -1
- package/lib/db/beacon.d.ts.map +1 -1
- package/lib/db/beacon.js +2 -0
- package/lib/db/beacon.js.map +1 -1
- package/lib/db/repositories/proposerSlashing.d.ts +1 -0
- package/lib/db/repositories/proposerSlashing.d.ts.map +1 -1
- package/lib/db/repositories/proposerSlashing.js +5 -1
- package/lib/db/repositories/proposerSlashing.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/network/metadata.d.ts +1 -1
- package/lib/network/metadata.d.ts.map +1 -1
- package/lib/network/metadata.js +17 -17
- package/lib/network/metadata.js.map +1 -1
- package/lib/network/peers/datastore.d.ts +9 -9
- package/lib/network/peers/datastore.d.ts.map +1 -1
- package/lib/network/peers/datastore.js +36 -36
- package/lib/network/peers/datastore.js.map +1 -1
- package/lib/network/processor/gossipHandlers.js +4 -4
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts.map +1 -1
- package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +41 -26
- package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
- package/lib/sync/unknownBlock.d.ts +9 -5
- package/lib/sync/unknownBlock.d.ts.map +1 -1
- package/lib/sync/unknownBlock.js +83 -42
- package/lib/sync/unknownBlock.js.map +1 -1
- package/lib/util/execution.d.ts +2 -0
- package/lib/util/execution.d.ts.map +1 -1
- package/lib/util/execution.js +1 -1
- package/lib/util/execution.js.map +1 -1
- package/package.json +15 -15
- package/src/api/impl/beacon/pool/index.ts +4 -4
- package/src/chain/archiveStore/archiveStore.ts +9 -3
- package/src/chain/archiveStore/utils/pruneHistory.ts +6 -4
- package/src/chain/chain.ts +3 -3
- package/src/chain/initState.ts +8 -60
- package/src/chain/opPools/opPool.ts +91 -18
- package/src/chain/opPools/utils.ts +31 -3
- package/src/chain/validation/attesterSlashing.ts +7 -4
- package/src/chain/validation/proposerSlashing.ts +7 -4
- package/src/db/beacon.ts +2 -0
- package/src/db/repositories/proposerSlashing.ts +6 -1
- package/src/index.ts +1 -1
- package/src/network/metadata.ts +17 -17
- package/src/network/peers/datastore.ts +36 -36
- package/src/network/processor/gossipHandlers.ts +4 -4
- package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +42 -28
- package/src/sync/unknownBlock.ts +94 -46
- package/src/util/execution.ts +1 -1
|
@@ -16,23 +16,23 @@ type AwaitGenerator<T, TReturn = any, TNext = any> = Generator<T, TReturn, TNext
|
|
|
16
16
|
* This has a memory issue because all peer data stays in memory and loaded at startup time
|
|
17
17
|
* This is written for libp2p >=0.35, we maintain the same mechanism but with bounded data structure
|
|
18
18
|
* This datastore includes a memory datastore and fallback to db datastore
|
|
19
|
-
* Use an in-memory datastore with last accessed time and
|
|
19
|
+
* Use an in-memory datastore with last accessed time and maxMemoryItems, on start it's empty (lazy load)
|
|
20
20
|
* - get: Search in-memory datastore first, if not found search from db.
|
|
21
21
|
* - If found from db, add back to the in-memory datastore
|
|
22
22
|
* - Update lastAccessedMs
|
|
23
|
-
* - put: move oldest items from memory to db if there are more than
|
|
24
|
-
* - update memory datastore, only update db datastore if there are at least
|
|
23
|
+
* - put: move oldest items from memory to db if there are more than maxMemoryItems items in memory
|
|
24
|
+
* - update memory datastore, only update db datastore if there are at least threshold dirty items
|
|
25
25
|
* - Update lastAccessedMs
|
|
26
26
|
*/
|
|
27
27
|
export class Eth2PeerDataStore extends BaseDatastore {
|
|
28
|
-
private
|
|
29
|
-
private
|
|
28
|
+
private dbDatastore: LevelDatastore;
|
|
29
|
+
private memoryDatastore: Map<string, MemoryItem>;
|
|
30
30
|
/** Same to PersistentPeerStore of the old libp2p implementation */
|
|
31
|
-
private
|
|
31
|
+
private dirtyItems = new Set<string>();
|
|
32
32
|
/** If there are more dirty items than threshold, commit data to db */
|
|
33
|
-
private
|
|
33
|
+
private threshold: number;
|
|
34
34
|
/** If there are more memory items than this, prune oldest ones from memory and move to db */
|
|
35
|
-
private
|
|
35
|
+
private maxMemoryItems: number;
|
|
36
36
|
|
|
37
37
|
constructor(
|
|
38
38
|
dbDatastore: LevelDatastore | string,
|
|
@@ -47,18 +47,18 @@ export class Eth2PeerDataStore extends BaseDatastore {
|
|
|
47
47
|
throw Error(`Threshold ${threshold} should be at most maxMemoryItems ${maxMemoryItems}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
this.
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
50
|
+
this.dbDatastore = typeof dbDatastore === "string" ? new LevelDatastore(dbDatastore) : dbDatastore;
|
|
51
|
+
this.memoryDatastore = new Map();
|
|
52
|
+
this.threshold = threshold;
|
|
53
|
+
this.maxMemoryItems = maxMemoryItems;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
async open(): Promise<void> {
|
|
57
|
-
return this.
|
|
57
|
+
return this.dbDatastore.open();
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
async close(): Promise<void> {
|
|
61
|
-
return this.
|
|
61
|
+
return this.dbDatastore.close();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async put(key: Key, val: Uint8Array, _options?: AbortOptions): Promise<Key> {
|
|
@@ -67,23 +67,23 @@ export class Eth2PeerDataStore extends BaseDatastore {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Same interface to put with "fromDb" option, if this item is updated back from db
|
|
70
|
-
* Move oldest items from memory data store to db if it's over this.
|
|
70
|
+
* Move oldest items from memory data store to db if it's over this.maxMemoryItems
|
|
71
71
|
*/
|
|
72
72
|
async _put(key: Key, val: Uint8Array, fromDb = false): Promise<Key> {
|
|
73
|
-
while (this.
|
|
73
|
+
while (this.memoryDatastore.size >= this.maxMemoryItems) {
|
|
74
74
|
// it's likely this is called only 1 time
|
|
75
75
|
await this.pruneMemoryDatastore();
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
const keyStr = key.toString();
|
|
79
|
-
const memoryItem = this.
|
|
79
|
+
const memoryItem = this.memoryDatastore.get(keyStr);
|
|
80
80
|
if (memoryItem) {
|
|
81
81
|
// update existing
|
|
82
82
|
memoryItem.lastAccessedMs = Date.now();
|
|
83
83
|
memoryItem.data = val;
|
|
84
84
|
} else {
|
|
85
85
|
// new
|
|
86
|
-
this.
|
|
86
|
+
this.memoryDatastore.set(keyStr, {data: val, lastAccessedMs: Date.now()});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
if (!fromDb) await this._addDirtyItem(keyStr);
|
|
@@ -98,15 +98,15 @@ export class Eth2PeerDataStore extends BaseDatastore {
|
|
|
98
98
|
*/
|
|
99
99
|
async get(key: Key, options?: AbortOptions): Promise<Uint8Array> {
|
|
100
100
|
const keyStr = key.toString();
|
|
101
|
-
const memoryItem = this.
|
|
101
|
+
const memoryItem = this.memoryDatastore.get(keyStr);
|
|
102
102
|
if (memoryItem) {
|
|
103
103
|
memoryItem.lastAccessedMs = Date.now();
|
|
104
104
|
return memoryItem.data;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// this throws error if not found
|
|
108
|
-
const dbValue = await this.
|
|
109
|
-
// don't call this.
|
|
108
|
+
const dbValue = await this.dbDatastore.get(key, options);
|
|
109
|
+
// don't call this.memoryDatastore.set directly
|
|
110
110
|
// we want to get through prune() logic with fromDb as true
|
|
111
111
|
await this._put(key, dbValue, true);
|
|
112
112
|
return dbValue;
|
|
@@ -125,30 +125,30 @@ export class Eth2PeerDataStore extends BaseDatastore {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
async delete(key: Key, options?: AbortOptions): Promise<void> {
|
|
128
|
-
this.
|
|
129
|
-
await this.
|
|
128
|
+
this.memoryDatastore.delete(key.toString());
|
|
129
|
+
await this.dbDatastore.delete(key, options);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
async *_all(q: Query, options?: AbortOptions): AwaitGenerator<Pair> {
|
|
133
|
-
for (const [key, value] of this.
|
|
133
|
+
for (const [key, value] of this.memoryDatastore.entries()) {
|
|
134
134
|
yield {
|
|
135
135
|
key: new Key(key),
|
|
136
136
|
value: value.data,
|
|
137
137
|
};
|
|
138
138
|
}
|
|
139
|
-
yield* this.
|
|
139
|
+
yield* this.dbDatastore.query(q, options);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
async *_allKeys(q: KeyQuery, options?: AbortOptions): AwaitGenerator<Key> {
|
|
143
|
-
for (const key of this.
|
|
143
|
+
for (const key of this.memoryDatastore.keys()) {
|
|
144
144
|
yield new Key(key);
|
|
145
145
|
}
|
|
146
|
-
yield* this.
|
|
146
|
+
yield* this.dbDatastore.queryKeys(q, options);
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
private async _addDirtyItem(keyStr: string): Promise<void> {
|
|
150
|
-
this.
|
|
151
|
-
if (this.
|
|
150
|
+
this.dirtyItems.add(keyStr);
|
|
151
|
+
if (this.dirtyItems.size >= this.threshold) {
|
|
152
152
|
try {
|
|
153
153
|
await this._commitData();
|
|
154
154
|
} catch (_e) {}
|
|
@@ -156,15 +156,15 @@ export class Eth2PeerDataStore extends BaseDatastore {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
private async _commitData(): Promise<void> {
|
|
159
|
-
const batch = this.
|
|
160
|
-
for (const keyStr of this.
|
|
161
|
-
const memoryItem = this.
|
|
159
|
+
const batch = this.dbDatastore.batch();
|
|
160
|
+
for (const keyStr of this.dirtyItems) {
|
|
161
|
+
const memoryItem = this.memoryDatastore.get(keyStr);
|
|
162
162
|
if (memoryItem) {
|
|
163
163
|
batch.put(new Key(keyStr), memoryItem.data);
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
await batch.commit();
|
|
167
|
-
this.
|
|
167
|
+
this.dirtyItems.clear();
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
/**
|
|
@@ -175,7 +175,7 @@ export class Eth2PeerDataStore extends BaseDatastore {
|
|
|
175
175
|
let oldestKey: string | undefined = undefined;
|
|
176
176
|
let oldestValue: Uint8Array | undefined = undefined;
|
|
177
177
|
|
|
178
|
-
for (const [key, value] of this.
|
|
178
|
+
for (const [key, value] of this.memoryDatastore) {
|
|
179
179
|
if (value.lastAccessedMs < oldestAccessedMs) {
|
|
180
180
|
oldestAccessedMs = value.lastAccessedMs;
|
|
181
181
|
oldestKey = key;
|
|
@@ -184,8 +184,8 @@ export class Eth2PeerDataStore extends BaseDatastore {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
if (oldestKey && oldestValue) {
|
|
187
|
-
await this.
|
|
188
|
-
this.
|
|
187
|
+
await this.dbDatastore.put(new Key(oldestKey), oldestValue);
|
|
188
|
+
this.memoryDatastore.delete(oldestKey);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
}
|
|
@@ -1065,12 +1065,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1065
1065
|
const {serializedData} = gossipData;
|
|
1066
1066
|
const {fork} = topic.boundary;
|
|
1067
1067
|
const attesterSlashing = sszDeserialize(topic, serializedData);
|
|
1068
|
-
await validateGossipAttesterSlashing(chain, attesterSlashing);
|
|
1068
|
+
const verifiedDomains = await validateGossipAttesterSlashing(chain, attesterSlashing);
|
|
1069
1069
|
|
|
1070
1070
|
// Handler - deferred to next event loop so the validation result propagates first
|
|
1071
1071
|
callInNextEventLoop(() => {
|
|
1072
1072
|
try {
|
|
1073
|
-
chain.opPool.insertAttesterSlashing(fork, attesterSlashing);
|
|
1073
|
+
chain.opPool.insertAttesterSlashing(fork, attesterSlashing, verifiedDomains);
|
|
1074
1074
|
chain.forkChoice.onAttesterSlashing(attesterSlashing);
|
|
1075
1075
|
chain.emitter.emit(routes.events.EventType.attesterSlashing, attesterSlashing);
|
|
1076
1076
|
} catch (e) {
|
|
@@ -1089,12 +1089,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1089
1089
|
}: GossipHandlerParamGeneric<GossipType.proposer_slashing>) => {
|
|
1090
1090
|
const {serializedData} = gossipData;
|
|
1091
1091
|
const proposerSlashing = sszDeserialize(topic, serializedData);
|
|
1092
|
-
await validateGossipProposerSlashing(chain, proposerSlashing);
|
|
1092
|
+
const verifiedDomain = await validateGossipProposerSlashing(chain, proposerSlashing);
|
|
1093
1093
|
|
|
1094
1094
|
// Handler - deferred to next event loop so the validation result propagates first
|
|
1095
1095
|
callInNextEventLoop(() => {
|
|
1096
1096
|
try {
|
|
1097
|
-
chain.opPool.insertProposerSlashing(proposerSlashing);
|
|
1097
|
+
chain.opPool.insertProposerSlashing(proposerSlashing, verifiedDomain);
|
|
1098
1098
|
chain.emitter.emit(routes.events.EventType.proposerSlashing, proposerSlashing);
|
|
1099
1099
|
} catch (e) {
|
|
1100
1100
|
logger.debug(
|
|
@@ -6,6 +6,7 @@ import {toRootHex} from "@lodestar/utils";
|
|
|
6
6
|
import {EnvelopeReconstructionError} from "../../../chain/errors/index.js";
|
|
7
7
|
import {IBeaconChain} from "../../../chain/index.js";
|
|
8
8
|
import {IBeaconDb} from "../../../db/index.js";
|
|
9
|
+
import {MAX_BODIES_PER_REQUEST} from "../../../util/execution.js";
|
|
9
10
|
import {ExecutionPayloadEnvelopesByRootRequest} from "../../../util/types.js";
|
|
10
11
|
import {prettyPrintPeerId} from "../../util.js";
|
|
11
12
|
|
|
@@ -19,10 +20,15 @@ export async function* onExecutionPayloadEnvelopesByRoot(
|
|
|
19
20
|
// The gloas req/resp spec uses MIN_EPOCHS_FOR_BLOCK_REQUESTS to define the minimum range peers MUST serve.
|
|
20
21
|
// Archival nodes may still serve older retained payloads to allow genesis sync.
|
|
21
22
|
|
|
22
|
-
// Resolve slots first so archived
|
|
23
|
+
// Resolve slots first so archived envelopes can be reconstructed in EL batches.
|
|
24
|
+
// Duplicate roots are served once, since each archived envelope costs an EL fetch and a rebuild.
|
|
23
25
|
const requests: {blockSlot: Slot; blockRootHex: RootHex}[] = [];
|
|
26
|
+
const seenRoots = new Set<RootHex>();
|
|
24
27
|
for (const root of requestBody) {
|
|
25
28
|
const rootHex = toRootHex(root);
|
|
29
|
+
if (seenRoots.has(rootHex)) continue;
|
|
30
|
+
seenRoots.add(rootHex);
|
|
31
|
+
|
|
26
32
|
const block = chain.forkChoice.getBlockHexDefaultStatus(rootHex);
|
|
27
33
|
// If the block is not in fork choice, it may be finalized. Attempt to find its slot in block archive
|
|
28
34
|
const slot = block ? block.slot : await db.blockArchive.getSlotByRoot(root);
|
|
@@ -41,35 +47,43 @@ export async function* onExecutionPayloadEnvelopesByRoot(
|
|
|
41
47
|
requests.push({blockSlot: slot, blockRootHex: rootHex});
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
50
|
+
// Yield between EL batches so the first response does not wait for the entire request.
|
|
51
|
+
let yielded = 0;
|
|
52
|
+
for (let i = 0; i < requests.length; i += MAX_BODIES_PER_REQUEST) {
|
|
53
|
+
const batch = requests.slice(i, i + MAX_BODIES_PER_REQUEST);
|
|
54
|
+
let envelopesBytes: (Uint8Array | null)[];
|
|
55
|
+
try {
|
|
56
|
+
// By-root allows omission, so a mismatched envelope is left out rather than failing the response.
|
|
57
|
+
envelopesBytes = await chain.getSerializedExecutionPayloadEnvelopes(batch, "omit");
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e instanceof EnvelopeReconstructionError) {
|
|
60
|
+
// By-root permits partial responses, so an EL failure can end the response here.
|
|
61
|
+
if (yielded > 0) return;
|
|
62
|
+
throw new ResponseError(
|
|
63
|
+
RespStatus.RESOURCE_UNAVAILABLE,
|
|
64
|
+
`Failed to reconstruct archived envelopes: ${e.message}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
throw e;
|
|
54
68
|
}
|
|
55
|
-
throw e;
|
|
56
|
-
}
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
for (let j = 0; j < batch.length; j++) {
|
|
71
|
+
const {blockSlot, blockRootHex} = batch[j];
|
|
72
|
+
const envelopeBytes = envelopesBytes[j];
|
|
73
|
+
if (envelopeBytes) {
|
|
74
|
+
yielded++;
|
|
75
|
+
yield {
|
|
76
|
+
data: envelopeBytes,
|
|
77
|
+
boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(blockSlot)),
|
|
78
|
+
};
|
|
79
|
+
} else {
|
|
80
|
+
chain.logger.debug("Cannot serve ExecutionPayloadEnvelopesByRoot: envelope not found", {
|
|
81
|
+
slot: blockSlot,
|
|
82
|
+
root: blockRootHex,
|
|
83
|
+
peer: prettyPrintPeerId(peerId),
|
|
84
|
+
client: peerClient,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
}
|
package/src/sync/unknownBlock.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {ChainEvent, ChainEventData, IBeaconChain} from "../chain/index.js";
|
|
|
15
15
|
import {validateGloasBlockDataColumnSidecars} from "../chain/validation/dataColumnSidecar.js";
|
|
16
16
|
import {validateGossipExecutionPayloadEnvelope} from "../chain/validation/executionPayloadEnvelope.js";
|
|
17
17
|
import {Metrics} from "../metrics/index.js";
|
|
18
|
-
import {INetwork, NetworkEvent, NetworkEventData, prettyPrintPeerIdStr} from "../network/index.js";
|
|
18
|
+
import {INetwork, NetworkEvent, NetworkEventData, PeerAction, prettyPrintPeerIdStr} from "../network/index.js";
|
|
19
19
|
import {PeerSyncMeta} from "../network/peers/peersData.js";
|
|
20
20
|
import {MAX_PEERS_PER_ROOT} from "../network/processor/constants.js";
|
|
21
21
|
import {ClockEvent} from "../util/clock.js";
|
|
@@ -442,6 +442,11 @@ export class BlockInputSync {
|
|
|
442
442
|
};
|
|
443
443
|
|
|
444
444
|
private addByRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr): boolean => {
|
|
445
|
+
if (this.knownBadBlocks.has(rootHex)) {
|
|
446
|
+
this.logger.debug("Ignoring known bad block root", {root: rootHex, peerIdStr: peerIdStr ?? "unknown peer"});
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
|
|
445
450
|
let pendingBlock = this.pendingBlocks.get(rootHex);
|
|
446
451
|
let added = false;
|
|
447
452
|
if (!pendingBlock) {
|
|
@@ -477,6 +482,11 @@ export class BlockInputSync {
|
|
|
477
482
|
};
|
|
478
483
|
|
|
479
484
|
private addByBlockInput = (blockInput: IBlockInput, peerIdStr?: string): void => {
|
|
485
|
+
if (this.knownBadBlocks.has(blockInput.blockRootHex)) {
|
|
486
|
+
this.logger.debug("Ignoring known bad block", {slot: blockInput.slot, root: blockInput.blockRootHex, peerIdStr});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
480
490
|
let pendingBlock = this.pendingBlocks.get(blockInput.blockRootHex);
|
|
481
491
|
// if entry is missing or was added via rootHex and now we have more complete information overwrite
|
|
482
492
|
// the existing information with the more complete cache entry
|
|
@@ -513,6 +523,15 @@ export class BlockInputSync {
|
|
|
513
523
|
};
|
|
514
524
|
|
|
515
525
|
private addByPayloadRootHex = (rootHex: RootHex, peerIdStr?: PeerIdStr, slot?: Slot): boolean => {
|
|
526
|
+
if (this.knownBadBlocks.has(rootHex)) {
|
|
527
|
+
this.logger.debug("Ignoring payload for known bad block root", {
|
|
528
|
+
slot: slot ?? "unknown",
|
|
529
|
+
root: rootHex,
|
|
530
|
+
peerIdStr,
|
|
531
|
+
});
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
|
|
516
535
|
let pendingPayload = this.pendingPayloads.get(rootHex);
|
|
517
536
|
let added = false;
|
|
518
537
|
if (!pendingPayload) {
|
|
@@ -581,6 +600,15 @@ export class BlockInputSync {
|
|
|
581
600
|
peerIdStr?: PeerIdStr,
|
|
582
601
|
envelope?: gloas.SignedExecutionPayloadEnvelope
|
|
583
602
|
): void => {
|
|
603
|
+
if (this.knownBadBlocks.has(payloadInput.blockRootHex)) {
|
|
604
|
+
this.logger.debug("Ignoring payload input for known bad block root", {
|
|
605
|
+
slot: payloadInput.slot,
|
|
606
|
+
root: payloadInput.blockRootHex,
|
|
607
|
+
peerIdStr,
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
584
612
|
const pendingPayload = this.toPendingPayloadInput(
|
|
585
613
|
payloadInput,
|
|
586
614
|
this.pendingPayloads.get(payloadInput.blockRootHex),
|
|
@@ -654,6 +682,7 @@ export class BlockInputSync {
|
|
|
654
682
|
return {kind: "ready"};
|
|
655
683
|
}
|
|
656
684
|
|
|
685
|
+
// The parent's payload is known but does not match `parentBlockHashHex`
|
|
657
686
|
if (this.chain.forkChoice.hasPayloadHexUnsafe(parentRootHex)) {
|
|
658
687
|
return {kind: "invalidParentPayload", parentRootHex, parentBlockHashHex};
|
|
659
688
|
}
|
|
@@ -1032,7 +1061,8 @@ export class BlockInputSync {
|
|
|
1032
1061
|
}
|
|
1033
1062
|
} else if (blockSlot <= finalizedSlot) {
|
|
1034
1063
|
// the common ancestor of the downloading chain and canonical chain should be at least the finalized slot and
|
|
1035
|
-
// we should found it through forkchoice. If not,
|
|
1064
|
+
// we should found it through forkchoice. If not, drop the chain without penalizing peers because pending entries are
|
|
1065
|
+
// block retained on failed download, so finalization can pass the block while it is still pending
|
|
1036
1066
|
// 0 - 1 - ... - n - finalizedSlot
|
|
1037
1067
|
// \
|
|
1038
1068
|
// parent 1 - parent 2 - ... - unknownParent block
|
|
@@ -1040,7 +1070,7 @@ export class BlockInputSync {
|
|
|
1040
1070
|
...logCtx2,
|
|
1041
1071
|
finalizedSlot,
|
|
1042
1072
|
});
|
|
1043
|
-
this.
|
|
1073
|
+
this.removeAllDescendants(block, DroppedItemReason.belowFinalized);
|
|
1044
1074
|
} else {
|
|
1045
1075
|
this.onUnknownBlockRoot({rootHex: pending.blockInput.parentRootHex, source: BlockInputSource.byRoot});
|
|
1046
1076
|
}
|
|
@@ -1172,15 +1202,18 @@ export class BlockInputSync {
|
|
|
1172
1202
|
break;
|
|
1173
1203
|
|
|
1174
1204
|
case BlockErrorCode.EXECUTION_ENGINE_INVALID:
|
|
1175
|
-
//
|
|
1205
|
+
// Ban but do not downscore: optimistic-sync peers may forward and serve blocks whose payload is later
|
|
1206
|
+
// rejected by the execution engine. Per the spec, an INVALID execution payload SHOULD NOT cause a peer
|
|
1207
|
+
// to be down-scored or disconnected, only consensus-layer invalidity MAY.
|
|
1208
|
+
// https://github.com/ethereum/consensus-specs/blob/master/specs/bellatrix/p2p-interface.md#the-reqresp-domain
|
|
1176
1209
|
this.logger.debug("Execution engine rejected block from unknown parent sync", errorData, res.err);
|
|
1177
|
-
this.
|
|
1210
|
+
this.removeAndBanAllDescendants(pendingBlock, DroppedItemReason.elInvalid);
|
|
1178
1211
|
break;
|
|
1179
1212
|
|
|
1180
1213
|
default:
|
|
1181
1214
|
// Block is not correct with respect to our chain. Log error loudly
|
|
1182
1215
|
this.logger.debug("Error processing block from unknown parent sync", errorData, res.err);
|
|
1183
|
-
this.
|
|
1216
|
+
this.removeAllDescendants(pendingBlock, DroppedItemReason.invalidBlock);
|
|
1184
1217
|
}
|
|
1185
1218
|
}
|
|
1186
1219
|
|
|
@@ -1478,11 +1511,11 @@ export class BlockInputSync {
|
|
|
1478
1511
|
}
|
|
1479
1512
|
|
|
1480
1513
|
const {peerId, client: peerClient} = peerMeta;
|
|
1481
|
-
cacheItem.peerIdStrings.add(peerId);
|
|
1482
1514
|
|
|
1483
1515
|
try {
|
|
1484
1516
|
if (!envelope) {
|
|
1485
1517
|
envelope = await this.fetchExecutionPayloadEnvelope(peerId, blockRoot, rootHex);
|
|
1518
|
+
cacheItem.peerIdStrings.add(peerId);
|
|
1486
1519
|
const slotWasUnknown = typeof slot !== "number";
|
|
1487
1520
|
slot = envelope.message.payload.slotNumber;
|
|
1488
1521
|
if (slotWasUnknown) {
|
|
@@ -1524,6 +1557,8 @@ export class BlockInputSync {
|
|
|
1524
1557
|
const missing = pendingPayload.payloadInput.getMissingSampledColumnMeta().missing;
|
|
1525
1558
|
if (missing.length > 0) {
|
|
1526
1559
|
const columnSidecars = await this.fetchPayloadColumns(peerMeta, pendingPayload.payloadInput, missing);
|
|
1560
|
+
// toPendingPayloadInput copied cacheItem's set, so add to the object that is carried forward
|
|
1561
|
+
pendingPayload.peerIdStrings.add(peerId);
|
|
1527
1562
|
const seenTimestampSec = Date.now() / 1000;
|
|
1528
1563
|
for (const columnSidecar of columnSidecars) {
|
|
1529
1564
|
if (pendingPayload.payloadInput.hasColumn(columnSidecar.index)) {
|
|
@@ -1716,8 +1751,6 @@ export class BlockInputSync {
|
|
|
1716
1751
|
}
|
|
1717
1752
|
const {peerId, client: peerClient} = peerMeta;
|
|
1718
1753
|
|
|
1719
|
-
cacheItem.peerIdStrings.add(peerId);
|
|
1720
|
-
|
|
1721
1754
|
try {
|
|
1722
1755
|
const downloadResult = await downloadByRoot({
|
|
1723
1756
|
config: this.config,
|
|
@@ -1728,6 +1761,7 @@ export class BlockInputSync {
|
|
|
1728
1761
|
cacheItem,
|
|
1729
1762
|
});
|
|
1730
1763
|
cacheItem = downloadResult.result;
|
|
1764
|
+
cacheItem.peerIdStrings.add(peerId);
|
|
1731
1765
|
if (slot === undefined) {
|
|
1732
1766
|
slot = cacheItem.blockInput.slot;
|
|
1733
1767
|
// we were not able to observe the time into slot when starting the fetch, do it now
|
|
@@ -1843,40 +1877,6 @@ export class BlockInputSync {
|
|
|
1843
1877
|
throw Error(message);
|
|
1844
1878
|
}
|
|
1845
1879
|
|
|
1846
|
-
/**
|
|
1847
|
-
* Gets all descendant blocks of `block` recursively from `pendingBlocks`.
|
|
1848
|
-
* Assumes that if a parent block does not exist or is not processable, all descendant blocks are bad too.
|
|
1849
|
-
* Downscore all peers that have referenced any of this bad blocks. May report peers multiple times if they have
|
|
1850
|
-
* referenced more than one bad block.
|
|
1851
|
-
*/
|
|
1852
|
-
private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem, headReason: DroppedItemReason): void {
|
|
1853
|
-
// Get all blocks that are a descendant of this one
|
|
1854
|
-
const badPendingBlocks = this.removeAllDescendants(block, headReason, DroppedItemReason.invalidParent);
|
|
1855
|
-
// just console log and do not penalize on pending/bad blocks for debugging
|
|
1856
|
-
// console.log("removeAndDownscoreAllDescendants", {block});
|
|
1857
|
-
|
|
1858
|
-
for (const block of badPendingBlocks) {
|
|
1859
|
-
//
|
|
1860
|
-
// TODO(fulu): why is this commented out here?
|
|
1861
|
-
//
|
|
1862
|
-
// this.knownBadBlocks.add(block.blockRootHex);
|
|
1863
|
-
// for (const peerIdStr of block.peerIdStrings) {
|
|
1864
|
-
// // TODO: Refactor peerRpcScores to work with peerIdStr only
|
|
1865
|
-
// this.network.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadBlockByRoot");
|
|
1866
|
-
// }
|
|
1867
|
-
this.logger.debug("ignored Banning unknown block", {
|
|
1868
|
-
slot: getBlockInputSyncCacheItemSlot(block),
|
|
1869
|
-
root: getBlockInputSyncCacheItemRootHex(block),
|
|
1870
|
-
peerIdStrings: Array.from(block.peerIdStrings)
|
|
1871
|
-
.map((id) => prettyPrintPeerIdStr(id))
|
|
1872
|
-
.join(","),
|
|
1873
|
-
});
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
// Prune knownBadBlocks
|
|
1877
|
-
pruneSetToMax(this.knownBadBlocks, MAX_KNOWN_BAD_BLOCKS);
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
1880
|
// Once a parent payload is invalid, every descendant waiting on that payload lineage becomes unrecoverable too.
|
|
1881
1881
|
private removePendingPayloadAndDescendants(rootHex: RootHex, headReason: DroppedItemReason): void {
|
|
1882
1882
|
// Keep PayloadEnvelopeInput resident in the seen cache. importBlock() owns that object and
|
|
@@ -1903,13 +1903,61 @@ export class BlockInputSync {
|
|
|
1903
1903
|
}
|
|
1904
1904
|
}
|
|
1905
1905
|
|
|
1906
|
+
/**
|
|
1907
|
+
* Same as `removeAndBanAllDescendants`, and downscores every peer that referenced or served a banned item, once per
|
|
1908
|
+
* peer. Only call for consensus-layer invalidity, never for an INVALID execution payload:
|
|
1909
|
+
* https://github.com/ethereum/consensus-specs/blob/master/specs/bellatrix/p2p-interface.md#the-reqresp-domain
|
|
1910
|
+
*/
|
|
1911
|
+
private removeAndDownScoreAllDescendants(block: BlockInputSyncCacheItem, headReason: DroppedItemReason): void {
|
|
1912
|
+
const peersToReport = new Set<PeerIdStr>();
|
|
1913
|
+
for (const bannedBlock of this.removeAndBanAllDescendants(block, headReason)) {
|
|
1914
|
+
for (const peerIdStr of bannedBlock.peerIdStrings) {
|
|
1915
|
+
peersToReport.add(peerIdStr);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
for (const peerIdStr of peersToReport) {
|
|
1920
|
+
this.network.reportPeer(peerIdStr, PeerAction.LowToleranceError, headReason);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
/**
|
|
1925
|
+
* Removes `block` and all its pending descendants and bans their roots in `knownBadBlocks`. Only call when the chain
|
|
1926
|
+
* is provably bad: a banned root is never searched for again. Returns the banned items.
|
|
1927
|
+
*/
|
|
1928
|
+
private removeAndBanAllDescendants(
|
|
1929
|
+
block: BlockInputSyncCacheItem,
|
|
1930
|
+
headReason: DroppedItemReason
|
|
1931
|
+
): BlockInputSyncCacheItem[] {
|
|
1932
|
+
const bannedBlocks: BlockInputSyncCacheItem[] = [];
|
|
1933
|
+
for (const badBlock of this.removeAllDescendants(block, headReason, DroppedItemReason.invalidParent)) {
|
|
1934
|
+
const rootHex = getBlockInputSyncCacheItemRootHex(badBlock);
|
|
1935
|
+
// a gossip import may race a failing sync path, never ban a root fork choice has imported
|
|
1936
|
+
if (this.chain.forkChoice.hasBlockHex(rootHex)) {
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
this.knownBadBlocks.add(rootHex);
|
|
1940
|
+
bannedBlocks.push(badBlock);
|
|
1941
|
+
this.logger.debug("Banning unknown block", {
|
|
1942
|
+
slot: getBlockInputSyncCacheItemSlot(badBlock),
|
|
1943
|
+
root: rootHex,
|
|
1944
|
+
reason: headReason,
|
|
1945
|
+
peerIdStrings: Array.from(badBlock.peerIdStrings)
|
|
1946
|
+
.map((id) => prettyPrintPeerIdStr(id))
|
|
1947
|
+
.join(","),
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
pruneSetToMax(this.knownBadBlocks, MAX_KNOWN_BAD_BLOCKS);
|
|
1952
|
+
return bannedBlocks;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1906
1955
|
private removeAllDescendants(
|
|
1907
1956
|
block: BlockInputSyncCacheItem,
|
|
1908
1957
|
headReason: DroppedItemReason,
|
|
1909
1958
|
descendantReason: DroppedItemReason = DroppedItemReason.invalidParent
|
|
1910
1959
|
): BlockInputSyncCacheItem[] {
|
|
1911
1960
|
const rootHex = getBlockInputSyncCacheItemRootHex(block);
|
|
1912
|
-
const slot = getBlockInputSyncCacheItemSlot(block);
|
|
1913
1961
|
// Get all blocks that are a descendant of this one (index 0 is the head item itself)
|
|
1914
1962
|
const badPendingBlocks = [block, ...getAllDescendantBlocks(rootHex, this.pendingBlocks)];
|
|
1915
1963
|
|
|
@@ -1926,8 +1974,8 @@ export class BlockInputSync {
|
|
|
1926
1974
|
// Keep PayloadEnvelopeInput resident in the seen cache for consistency with the
|
|
1927
1975
|
// importBlock()-owned lifecycle.
|
|
1928
1976
|
this.logger.debug("Removing bad/unknown/incomplete BlockInputSyncCacheItem", {
|
|
1929
|
-
slot,
|
|
1930
|
-
|
|
1977
|
+
slot: getBlockInputSyncCacheItemSlot(block),
|
|
1978
|
+
root: rootHex,
|
|
1931
1979
|
});
|
|
1932
1980
|
}
|
|
1933
1981
|
|
package/src/util/execution.ts
CHANGED
|
@@ -260,7 +260,7 @@ export async function getDataColumnSidecarsFromExecution(
|
|
|
260
260
|
}
|
|
261
261
|
|
|
262
262
|
/** engine_getPayloadBodiesByHashV2: every EL must accept requests of up to 32 hashes, larger ones may fail with -38004 */
|
|
263
|
-
const MAX_BODIES_PER_REQUEST = 32;
|
|
263
|
+
export const MAX_BODIES_PER_REQUEST = 32;
|
|
264
264
|
|
|
265
265
|
type SlotEnvelopeBytes = {slot: Slot; envelopeBytes: Uint8Array};
|
|
266
266
|
|