@dashevo/dapi-client 5.0.3 → 5.1.0-dev.1
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/BlockHeadersProvider/BlockHeadersProvider.js +158 -24
- package/lib/BlockHeadersProvider/BlockHeadersReader.js +264 -44
- package/lib/dapiAddressProvider/ListDAPIAddressProvider.js +1 -23
- package/lib/dapiAddressProvider/SimplifiedMasternodeListDAPIAddressProvider.js +17 -1
- package/lib/dapiAddressProvider/createDAPIAddressProviderFromOptions.js +1 -0
- package/lib/networkConfigs.js +1 -1
- package/lib/test/mocks/BlockHeadersWithChainLocksStreamMock.js +2 -2
- package/lib/transport/ReconnectableStream.js +3 -2
- package/package.json +6 -6
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
const EventEmitter = require('events');
|
|
2
2
|
const { SpvChain, SPVError } = require('@dashevo/dash-spv');
|
|
3
|
+
const GrpcErrorCodes = require('@dashevo/grpc-common/lib/server/error/GrpcErrorCodes');
|
|
3
4
|
|
|
4
5
|
const BlockHeadersReader = require('./BlockHeadersReader');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* @typedef {BlockHeadersProviderOptions} BlockHeadersProviderOptions
|
|
8
9
|
* @property {string} [network='testnet'] network type
|
|
9
|
-
* @property {number} [maxParallelStreams=
|
|
10
|
-
* @property {number} [targetBatchSize=
|
|
10
|
+
* @property {number} [maxParallelStreams=10] max parallel streams to read historical block headers
|
|
11
|
+
* @property {number} [targetBatchSize=10000] a target batch size per stream
|
|
11
12
|
* @property {number} [fromBlockHeight=1] a target batch size per stream
|
|
12
13
|
* @property {number} [maxRetries=10] max amount of retries per stream connection
|
|
13
14
|
* @property {number} [powTargetSpacing] devnets only: the -powtargetspacing the
|
|
@@ -18,7 +19,9 @@ const BlockHeadersReader = require('./BlockHeadersReader');
|
|
|
18
19
|
const defaultOptions = {
|
|
19
20
|
network: 'testnet',
|
|
20
21
|
maxParallelStreams: 10,
|
|
21
|
-
|
|
22
|
+
// Headers read ahead of the chain wait in memory, up to about
|
|
23
|
+
// maxParallelStreams * targetBatchSize of them
|
|
24
|
+
targetBatchSize: 10000,
|
|
22
25
|
fromBlockHeight: 1,
|
|
23
26
|
maxRetries: 10,
|
|
24
27
|
};
|
|
@@ -53,19 +56,25 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
53
56
|
...options,
|
|
54
57
|
};
|
|
55
58
|
|
|
56
|
-
const { powTargetSpacing, minimumDifficultyBlocks } = this.options;
|
|
57
|
-
const devnetOptions = powTargetSpacing !== undefined || minimumDifficultyBlocks !== undefined
|
|
58
|
-
? { powTargetSpacing, minimumDifficultyBlocks }
|
|
59
|
-
: undefined;
|
|
60
|
-
this.spvChain = new SpvChain(this.options.network, 100, devnetOptions);
|
|
61
|
-
|
|
62
59
|
this.state = STATES.IDLE;
|
|
63
60
|
|
|
61
|
+
// How far below the tip continuous sync asks for headers again when new
|
|
62
|
+
// ones don't connect: DAPI streams only each new tip, so after a reorg or
|
|
63
|
+
// several blocks at once the parents have to be fetched by height
|
|
64
|
+
this.continuousLookback = 1;
|
|
65
|
+
|
|
64
66
|
this.errorHandler = this.errorHandler.bind(this);
|
|
65
67
|
this.headersHandler = this.headersHandler.bind(this);
|
|
68
|
+
this.chainConnectedHandler = this.chainConnectedHandler.bind(this);
|
|
66
69
|
this.historicalDataObtainedHandler = this.historicalDataObtainedHandler.bind(this);
|
|
67
70
|
this.createHistoricalSyncStream = createHistoricalSyncStream;
|
|
68
71
|
this.createContinuousSyncStream = createContinuousSyncStream;
|
|
72
|
+
|
|
73
|
+
const { powTargetSpacing, minimumDifficultyBlocks } = this.options;
|
|
74
|
+
const devnetOptions = powTargetSpacing !== undefined || minimumDifficultyBlocks !== undefined
|
|
75
|
+
? { powTargetSpacing, minimumDifficultyBlocks }
|
|
76
|
+
: undefined;
|
|
77
|
+
this.setSpvChain(new SpvChain(this.options.network, 100, devnetOptions));
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
@@ -80,7 +89,11 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
80
89
|
* @param {SpvChain} spvChain instance
|
|
81
90
|
*/
|
|
82
91
|
setSpvChain(spvChain) {
|
|
92
|
+
if (this.spvChain) {
|
|
93
|
+
this.spvChain.removeListener(SpvChain.EVENTS.CONNECTED, this.chainConnectedHandler);
|
|
94
|
+
}
|
|
83
95
|
this.spvChain = spvChain;
|
|
96
|
+
this.spvChain.on(SpvChain.EVENTS.CONNECTED, this.chainConnectedHandler);
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
/**
|
|
@@ -96,6 +109,9 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
96
109
|
maxParallelStreams: this.options.maxParallelStreams,
|
|
97
110
|
targetBatchSize: this.options.targetBatchSize,
|
|
98
111
|
maxRetries: this.options.maxRetries,
|
|
112
|
+
getConnectedHeight: () => (this.spvChain.startBlockHeight === null
|
|
113
|
+
? -Infinity
|
|
114
|
+
: this.spvChain.getTipHeight()),
|
|
99
115
|
},
|
|
100
116
|
this.createHistoricalSyncStream,
|
|
101
117
|
this.createContinuousSyncStream,
|
|
@@ -159,10 +175,16 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
159
175
|
this.blockHeadersReader
|
|
160
176
|
.once(BlockHeadersReader.EVENTS.HISTORICAL_DATA_OBTAINED, this.historicalDataObtainedHandler);
|
|
161
177
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
178
|
+
try {
|
|
179
|
+
await this.blockHeadersReader.readHistorical(
|
|
180
|
+
fromBlockHeight,
|
|
181
|
+
toBlockHeight,
|
|
182
|
+
);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// init() adds them again on the next attempt
|
|
185
|
+
this.removeReaderListeners();
|
|
186
|
+
throw e;
|
|
187
|
+
}
|
|
166
188
|
|
|
167
189
|
this.state = STATES.HISTORICAL_SYNC;
|
|
168
190
|
this.emit(EVENTS.HISTORICAL_SYNC_STARTED);
|
|
@@ -175,7 +197,18 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
175
197
|
|
|
176
198
|
await this.init();
|
|
177
199
|
this.ensureChainRoot(fromBlockHeight);
|
|
178
|
-
|
|
200
|
+
this.continuousLookback = 1;
|
|
201
|
+
this.continuousGapReported = false;
|
|
202
|
+
try {
|
|
203
|
+
await this.blockHeadersReader.subscribeToNew(
|
|
204
|
+
fromBlockHeight,
|
|
205
|
+
(retryError) => this.getContinuousRetryHeight(retryError),
|
|
206
|
+
);
|
|
207
|
+
} catch (e) {
|
|
208
|
+
// init() adds them again on the next attempt
|
|
209
|
+
this.removeReaderListeners();
|
|
210
|
+
throw e;
|
|
211
|
+
}
|
|
179
212
|
this.state = STATES.CONTINUOUS_SYNC;
|
|
180
213
|
this.emit(EVENTS.CONTINUOUS_SYNC_STARTED);
|
|
181
214
|
}
|
|
@@ -225,22 +258,123 @@ class BlockHeadersProvider extends EventEmitter {
|
|
|
225
258
|
// BlockHeadersReader might not correctly set startBlockHeight
|
|
226
259
|
// which results in orphan chunks in SPV
|
|
227
260
|
|
|
261
|
+
let rejection = null;
|
|
262
|
+
let gapError = null;
|
|
228
263
|
try {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
this.
|
|
264
|
+
// Headers that reach the main chain come back through chainConnectedHandler
|
|
265
|
+
this.spvChain.addHeaders(headers, headHeight);
|
|
266
|
+
|
|
267
|
+
if (this.state === STATES.CONTINUOUS_SYNC && this.spvChain.getOrphanChunks().length) {
|
|
268
|
+
// Their parents were never sent: ask again from further below the tip
|
|
269
|
+
this.spvChain.dropOrphans();
|
|
270
|
+
if (this.widenContinuousLookback()) {
|
|
271
|
+
gapError = new SPVError(`New headers don't connect to the chain even ${this.spvChain.confirmsBeforeFinal + 1} headers below its tip`);
|
|
272
|
+
}
|
|
273
|
+
rejection = new SPVError(`New headers don't connect to the chain, requesting them again from height ${this.getContinuousResumeHeight()}`);
|
|
236
274
|
}
|
|
237
275
|
} catch (e) {
|
|
238
|
-
if (e instanceof SPVError) {
|
|
239
|
-
reject(e);
|
|
240
|
-
} else {
|
|
276
|
+
if (!(e instanceof SPVError)) {
|
|
241
277
|
this.errorHandler(e);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
rejection = e;
|
|
281
|
+
if (this.state === STATES.CONTINUOUS_SYNC && this.isAtMaxLookback()) {
|
|
282
|
+
// Such as a reorg that would replace confirmed headers
|
|
283
|
+
gapError = e;
|
|
242
284
|
}
|
|
243
285
|
}
|
|
286
|
+
|
|
287
|
+
// Retry once, then report, outside the validation above
|
|
288
|
+
if (rejection) {
|
|
289
|
+
reject(rejection);
|
|
290
|
+
}
|
|
291
|
+
if (gapError) {
|
|
292
|
+
this.reportContinuousGap(gapError);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @private
|
|
298
|
+
* @param {BlockHeader[]} headers - the main chain from firstHeight on;
|
|
299
|
+
* after a reorg firstHeight is at or below the previous tip
|
|
300
|
+
* @param {number} firstHeight
|
|
301
|
+
*/
|
|
302
|
+
chainConnectedHandler(headers, firstHeight) {
|
|
303
|
+
if (this.blockHeadersReader && typeof this.blockHeadersReader.chainAdvanced === 'function') {
|
|
304
|
+
this.blockHeadersReader.chainAdvanced();
|
|
305
|
+
}
|
|
306
|
+
this.continuousLookback = 1;
|
|
307
|
+
this.continuousGapReported = false;
|
|
308
|
+
this.emit(EVENTS.CHAIN_UPDATED, headers, firstHeight);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* @private
|
|
313
|
+
* Continuous sync keeps retrying headers it can't add, but reports once,
|
|
314
|
+
* after looking back as far as it can, that it can't get back on the chain
|
|
315
|
+
* @param {Error} error
|
|
316
|
+
*/
|
|
317
|
+
reportContinuousGap(error) {
|
|
318
|
+
if (!this.continuousGapReported && this.listenerCount(EVENTS.ERROR) > 0) {
|
|
319
|
+
this.continuousGapReported = true;
|
|
320
|
+
this.emit(EVENTS.ERROR, error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** @private */
|
|
325
|
+
isAtMaxLookback() {
|
|
326
|
+
return this.continuousLookback === this.spvChain.confirmsBeforeFinal + 1;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @private
|
|
331
|
+
* Makes continuous sync resubscribe from further below the tip
|
|
332
|
+
* @return {boolean} whether it already looked back as far as it can
|
|
333
|
+
*/
|
|
334
|
+
widenContinuousLookback() {
|
|
335
|
+
const wasAtMax = this.isAtMaxLookback();
|
|
336
|
+
this.continuousLookback = Math.min(
|
|
337
|
+
this.continuousLookback * 2,
|
|
338
|
+
this.spvChain.confirmsBeforeFinal + 1,
|
|
339
|
+
);
|
|
340
|
+
return wasAtMax;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @private
|
|
345
|
+
* Height continuous sync resubscribes from after the stream failed
|
|
346
|
+
* @param {Error} [retryError] - the error that made the stream retry
|
|
347
|
+
* @return {number}
|
|
348
|
+
*/
|
|
349
|
+
getContinuousRetryHeight(retryError) {
|
|
350
|
+
// DAPI has no block at the height asked for, as after a reorg to a
|
|
351
|
+
// shorter chain with more work; without headers arriving, only this
|
|
352
|
+
// moves the next request below the new tip
|
|
353
|
+
if (retryError && retryError.code === GrpcErrorCodes.NOT_FOUND
|
|
354
|
+
&& this.state === STATES.CONTINUOUS_SYNC && this.spvChain.startBlockHeight !== null
|
|
355
|
+
&& this.widenContinuousLookback()) {
|
|
356
|
+
const gapError = new Error(`DAPI has no block even ${this.spvChain.confirmsBeforeFinal + 1} headers below the chain tip: ${retryError.message}`);
|
|
357
|
+
// The stream schedules its retry after this returns: report once it
|
|
358
|
+
// has, so an error listener that throws can't stop the retry
|
|
359
|
+
setTimeout(() => {
|
|
360
|
+
if (this.state === STATES.CONTINUOUS_SYNC) {
|
|
361
|
+
this.reportContinuousGap(gapError);
|
|
362
|
+
}
|
|
363
|
+
}, 0);
|
|
364
|
+
}
|
|
365
|
+
return this.getContinuousResumeHeight();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* @private
|
|
370
|
+
* @return {number}
|
|
371
|
+
*/
|
|
372
|
+
getContinuousResumeHeight() {
|
|
373
|
+
if (this.spvChain.startBlockHeight === null) {
|
|
374
|
+
// No headers yet: wait for the first one where sync was asked to start
|
|
375
|
+
return this.spvChain.pendingStartBlockHeight || 1;
|
|
376
|
+
}
|
|
377
|
+
return Math.max(1, this.spvChain.getTipHeight() - this.continuousLookback + 1);
|
|
244
378
|
}
|
|
245
379
|
|
|
246
380
|
historicalDataObtainedHandler() {
|
|
@@ -15,6 +15,8 @@ const CLIENT_ALREADY_CLOSED_ERROR_MESSAGE = 'Client already closed - cannot .clo
|
|
|
15
15
|
* @property {number} [maxParallelStreams]
|
|
16
16
|
* @property {number} [targetBatchSize]
|
|
17
17
|
* @property {number} [maxRetries]
|
|
18
|
+
* @property {function(): number} [getConnectedHeight] - height up to which
|
|
19
|
+
* headers joined the chain; bounds how far historical reads run ahead
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
22
|
class BlockHeadersReader extends EventEmitter {
|
|
@@ -32,13 +34,31 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
32
34
|
this.maxParallelStreams = options.maxParallelStreams;
|
|
33
35
|
this.targetBatchSize = options.targetBatchSize;
|
|
34
36
|
this.maxRetries = options.maxRetries;
|
|
37
|
+
this.getConnectedHeight = options.getConnectedHeight;
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
|
-
* Holds references to the historical streams
|
|
40
|
+
* Holds references to the running historical streams, in batch order
|
|
38
41
|
* @type {Stream[]}
|
|
39
42
|
*/
|
|
40
43
|
this.historicalStreams = [];
|
|
41
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Batches of the current historical read that started and haven't
|
|
47
|
+
* settled yet, in height order; null when not reading
|
|
48
|
+
* @type {{
|
|
49
|
+
* fromBlockHeight: number, count: number, stream: ?Stream, retry: ?Function, done: boolean
|
|
50
|
+
* }[]|null}
|
|
51
|
+
*/
|
|
52
|
+
this.historicalBatches = null;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Where the current historical read's next batch starts, how large its
|
|
56
|
+
* batches are, and its last height
|
|
57
|
+
*/
|
|
58
|
+
this.nextBatchHeight = 0;
|
|
59
|
+
this.historicalBatchSize = 0;
|
|
60
|
+
this.historicalToBlockHeight = 0;
|
|
61
|
+
|
|
42
62
|
/**
|
|
43
63
|
* Holds reference to the continuous sync stream
|
|
44
64
|
* @type {Stream}
|
|
@@ -47,13 +67,17 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
47
67
|
}
|
|
48
68
|
|
|
49
69
|
/**
|
|
50
|
-
* Reads historical block
|
|
70
|
+
* Reads historical block headers in batches of about targetBatchSize, with
|
|
71
|
+
* up to maxParallelStreams batches in flight. A batch starts only within
|
|
72
|
+
* maxParallelStreams batches of the first one not yet settled: finished and,
|
|
73
|
+
* with getConnectedHeight, joined to the chain. So headers waiting ahead of
|
|
74
|
+
* the chain stay bounded.
|
|
51
75
|
* @param {number} fromBlockHeight
|
|
52
76
|
* @param {number} toBlockHeight
|
|
53
77
|
* @returns {Promise<void>}
|
|
54
78
|
*/
|
|
55
79
|
async readHistorical(fromBlockHeight, toBlockHeight) {
|
|
56
|
-
if (this.
|
|
80
|
+
if (this.historicalBatches) {
|
|
57
81
|
throw new Error('Historical streams are already running. Please stop them first.');
|
|
58
82
|
}
|
|
59
83
|
|
|
@@ -66,37 +90,173 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
66
90
|
throw new Error(`Invalid total amount of headers to read: ${totalAmount}`);
|
|
67
91
|
}
|
|
68
92
|
|
|
69
|
-
const
|
|
70
|
-
Math.max(Math.round(totalAmount / this.targetBatchSize), 1),
|
|
71
|
-
this.maxParallelStreams,
|
|
72
|
-
);
|
|
93
|
+
const batchCount = Math.max(Math.round(totalAmount / this.targetBatchSize), 1);
|
|
73
94
|
|
|
74
|
-
|
|
95
|
+
// Batches are made as the window reaches them: toBlockHeight comes from
|
|
96
|
+
// a DAPI node, so the read must not allocate per batch up front
|
|
97
|
+
this.historicalBatches = [];
|
|
98
|
+
this.historicalBatchSize = Math.ceil(totalAmount / batchCount);
|
|
99
|
+
this.historicalToBlockHeight = toBlockHeight;
|
|
100
|
+
this.nextBatchHeight = fromBlockHeight;
|
|
75
101
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
try {
|
|
103
|
+
await this.startHistoricalBatches();
|
|
104
|
+
} catch (e) {
|
|
105
|
+
this.stopReadingHistorical();
|
|
106
|
+
throw e;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @private
|
|
112
|
+
* Starts the batches the window allows
|
|
113
|
+
*/
|
|
114
|
+
async startHistoricalBatches() {
|
|
115
|
+
const batches = this.historicalBatches;
|
|
116
|
+
while (batches === this.historicalBatches
|
|
117
|
+
&& this.nextBatchHeight <= this.historicalToBlockHeight
|
|
118
|
+
&& batches.length < this.maxParallelStreams) {
|
|
119
|
+
const batch = {
|
|
120
|
+
fromBlockHeight: this.nextBatchHeight,
|
|
121
|
+
count: Math.min(
|
|
122
|
+
this.historicalBatchSize,
|
|
123
|
+
this.historicalToBlockHeight - this.nextBatchHeight + 1,
|
|
124
|
+
),
|
|
125
|
+
stream: null,
|
|
126
|
+
retry: null,
|
|
127
|
+
done: false,
|
|
128
|
+
};
|
|
129
|
+
batches.push(batch);
|
|
130
|
+
this.nextBatchHeight += batch.count;
|
|
79
131
|
|
|
80
|
-
const subscribeWithRetries = this.createSubscribeToHistoricalBatch(this.maxRetries);
|
|
132
|
+
const subscribeWithRetries = this.createSubscribeToHistoricalBatch(this.maxRetries, batch);
|
|
81
133
|
// eslint-disable-next-line no-await-in-loop
|
|
82
|
-
const stream = await subscribeWithRetries(
|
|
83
|
-
this.
|
|
134
|
+
const stream = await subscribeWithRetries(batch.fromBlockHeight, batch.count);
|
|
135
|
+
if (batches === this.historicalBatches) {
|
|
136
|
+
batch.stream = stream;
|
|
137
|
+
this.updateHistoricalStreams();
|
|
138
|
+
} else {
|
|
139
|
+
// Stopped while subscribing
|
|
140
|
+
this.cancelStream(stream);
|
|
141
|
+
}
|
|
84
142
|
}
|
|
85
143
|
}
|
|
86
144
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
145
|
+
/** @private */
|
|
146
|
+
updateHistoricalStreams() {
|
|
147
|
+
this.historicalStreams = (this.historicalBatches || [])
|
|
148
|
+
.filter(({ stream }) => stream)
|
|
149
|
+
.map(({ stream }) => stream);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @private
|
|
154
|
+
* @param {object} batch
|
|
155
|
+
* @param {Stream} stream
|
|
156
|
+
*/
|
|
157
|
+
historicalBatchEnded(batch, stream) {
|
|
158
|
+
if (!batch) {
|
|
159
|
+
// A stream read on its own
|
|
160
|
+
this.emit(EVENTS.HISTORICAL_DATA_OBTAINED);
|
|
161
|
+
return;
|
|
91
162
|
}
|
|
163
|
+
|
|
164
|
+
if (batch.stream !== stream || !this.historicalBatches) {
|
|
165
|
+
// Ended after a retry replaced it or reading stopped
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* eslint-disable no-param-reassign */
|
|
170
|
+
batch.stream = null;
|
|
171
|
+
batch.done = true;
|
|
172
|
+
/* eslint-enable no-param-reassign */
|
|
173
|
+
this.updateHistoricalStreams();
|
|
174
|
+
|
|
175
|
+
const batches = this.historicalBatches;
|
|
176
|
+
if (this.nextBatchHeight > this.historicalToBlockHeight && batches.every(({ done }) => done)) {
|
|
177
|
+
// Every batch arrived; did those not settled yet all join the chain?
|
|
178
|
+
const connectedHeight = this.getConnectedHeight ? this.getConnectedHeight() : Infinity;
|
|
179
|
+
const unconnected = batches.find(({ fromBlockHeight, count }) => (
|
|
180
|
+
connectedHeight < fromBlockHeight + count - 1));
|
|
181
|
+
if (unconnected) {
|
|
182
|
+
this.stopReadingHistorical();
|
|
183
|
+
this.emit(EVENTS.ERROR, new Error(`Headers from height ${unconnected.fromBlockHeight} don't connect to the chain`));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.historicalBatches = null;
|
|
187
|
+
this.emit(EVENTS.HISTORICAL_DATA_OBTAINED);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.advanceHistoricalWindow();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Lets historical reads run further ahead once headers join the chain
|
|
196
|
+
*/
|
|
197
|
+
chainAdvanced() {
|
|
198
|
+
if (this.historicalBatches) {
|
|
199
|
+
this.advanceHistoricalWindow();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** @private */
|
|
204
|
+
advanceHistoricalWindow() {
|
|
205
|
+
const batches = this.historicalBatches;
|
|
206
|
+
const connectedHeight = this.getConnectedHeight ? this.getConnectedHeight() : Infinity;
|
|
207
|
+
// Retire the batches that arrived and joined the chain
|
|
208
|
+
while (batches.length) {
|
|
209
|
+
const { done, fromBlockHeight, count } = batches[0];
|
|
210
|
+
if (!done || connectedHeight < fromBlockHeight + count - 1) {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
batches.shift();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const windowFull = batches.length >= this.maxParallelStreams;
|
|
217
|
+
const inFlight = batches.some(({ done }) => !done);
|
|
218
|
+
if (windowFull && !inFlight) {
|
|
219
|
+
// Everything in the window arrived, but the first batch didn't connect
|
|
220
|
+
const { fromBlockHeight } = batches[0];
|
|
221
|
+
this.stopReadingHistorical();
|
|
222
|
+
this.emit(EVENTS.ERROR, new Error(`Headers from height ${fromBlockHeight} don't connect to the chain`));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.startHistoricalBatches().catch((e) => {
|
|
227
|
+
if (batches !== this.historicalBatches) {
|
|
228
|
+
// A later read replaced this one
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.stopReadingHistorical();
|
|
232
|
+
this.emit(EVENTS.ERROR, e);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
stopReadingHistorical() {
|
|
237
|
+
const streams = this.historicalStreams;
|
|
238
|
+
// Detach the batches, so retries in flight cancel the streams they open
|
|
239
|
+
(this.historicalBatches || []).forEach((batch) => {
|
|
240
|
+
/* eslint-disable no-param-reassign */
|
|
241
|
+
batch.stream = null;
|
|
242
|
+
batch.retry = null;
|
|
243
|
+
/* eslint-enable no-param-reassign */
|
|
244
|
+
});
|
|
245
|
+
this.historicalBatches = null;
|
|
246
|
+
this.historicalStreams = [];
|
|
247
|
+
streams.forEach((stream) => this.cancelStream(stream));
|
|
92
248
|
}
|
|
93
249
|
|
|
94
250
|
/**
|
|
95
251
|
* Subscribes to continuously arriving block headers
|
|
96
252
|
* @param {number} fromBlockHeight
|
|
253
|
+
* @param {function(Error=): number} [getResumeHeight] - height to resubscribe
|
|
254
|
+
* from after the stream drops or headers are rejected, given the error that
|
|
255
|
+
* caused the retry if there was one; by default the height of the last
|
|
256
|
+
* header received
|
|
97
257
|
* @returns {Promise<ReconnectableStream>}
|
|
98
258
|
*/
|
|
99
|
-
async subscribeToNew(fromBlockHeight) {
|
|
259
|
+
async subscribeToNew(fromBlockHeight, getResumeHeight) {
|
|
100
260
|
if (this.continuousSyncStream) {
|
|
101
261
|
throw new Error('Continuous sync has already been started');
|
|
102
262
|
}
|
|
@@ -147,13 +307,14 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
147
307
|
}
|
|
148
308
|
};
|
|
149
309
|
|
|
150
|
-
const beforeReconnectHandler = (updateArguments) => {
|
|
310
|
+
const beforeReconnectHandler = (updateArguments, retryError) => {
|
|
311
|
+
const resumeHeight = getResumeHeight ? getResumeHeight(retryError) : lastKnownChainHeight;
|
|
151
312
|
updateArguments({
|
|
152
|
-
fromBlockHeight:
|
|
313
|
+
fromBlockHeight: resumeHeight,
|
|
153
314
|
count: 0,
|
|
154
315
|
});
|
|
155
316
|
|
|
156
|
-
lastKnownChainHeight
|
|
317
|
+
lastKnownChainHeight = resumeHeight - 1;
|
|
157
318
|
};
|
|
158
319
|
|
|
159
320
|
const endHandler = () => {
|
|
@@ -189,9 +350,10 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
189
350
|
* and handles retry logic
|
|
190
351
|
* @private
|
|
191
352
|
* @param {number} [maxRetries] - maximum amount of retries
|
|
353
|
+
* @param {object} [batch] - the batch the stream reads, from readHistorical
|
|
192
354
|
* @returns {function(*, *): Promise<Stream>}
|
|
193
355
|
*/
|
|
194
|
-
createSubscribeToHistoricalBatch(maxRetries = 0) {
|
|
356
|
+
createSubscribeToHistoricalBatch(maxRetries = 0, batch = null) {
|
|
195
357
|
let currentRetries = 0;
|
|
196
358
|
|
|
197
359
|
/**
|
|
@@ -205,28 +367,64 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
205
367
|
|
|
206
368
|
const stream = await this.createHistoricalSyncStream(fromBlockHeight, count);
|
|
207
369
|
|
|
370
|
+
// Events of a stream its batch no longer reads from (it failed and a
|
|
371
|
+
// retry replaced it, reading stopped, or it already delivered its batch)
|
|
372
|
+
// are ignored. The reader detaches a stream before it cancels it, so a
|
|
373
|
+
// CANCELLED error from a current stream came from the server and is
|
|
374
|
+
// retried like any other.
|
|
375
|
+
let finished = false;
|
|
376
|
+
const isCurrent = () => !finished && (!batch || batch.stream === stream);
|
|
377
|
+
|
|
208
378
|
const errorHandler = (streamError) => {
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
379
|
+
if (!isCurrent()) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (headersObtained >= count) {
|
|
384
|
+
// It already delivered its whole batch
|
|
385
|
+
this.historicalBatchEnded(batch, stream);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!batch && streamError.code === GrpcErrorCodes.CANCELLED) {
|
|
216
390
|
return;
|
|
217
391
|
}
|
|
218
392
|
|
|
219
393
|
if (currentRetries < maxRetries) {
|
|
220
394
|
const newFromBlockHeight = fromBlockHeight + headersObtained;
|
|
221
395
|
const newCount = count - headersObtained;
|
|
396
|
+
currentRetries += 1;
|
|
397
|
+
|
|
398
|
+
// Detach the failed stream at once, so its late events can't end
|
|
399
|
+
// the batch while the retry connects
|
|
400
|
+
const retry = {};
|
|
401
|
+
if (batch) {
|
|
402
|
+
/* eslint-disable no-param-reassign */
|
|
403
|
+
batch.stream = null;
|
|
404
|
+
batch.retry = retry;
|
|
405
|
+
/* eslint-enable no-param-reassign */
|
|
406
|
+
this.updateHistoricalStreams();
|
|
407
|
+
}
|
|
222
408
|
|
|
223
|
-
// TODO: do not retry in case newCount is zero
|
|
224
409
|
subscribeWithRetries(newFromBlockHeight, newCount)
|
|
225
410
|
.then((newStream) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
411
|
+
if (!batch) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (batch.retry !== retry) {
|
|
415
|
+
// Reading stopped in the meantime
|
|
416
|
+
this.cancelStream(newStream);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
/* eslint-disable no-param-reassign */
|
|
420
|
+
batch.retry = null;
|
|
421
|
+
batch.stream = newStream;
|
|
422
|
+
/* eslint-enable no-param-reassign */
|
|
423
|
+
this.updateHistoricalStreams();
|
|
229
424
|
}).catch((e) => {
|
|
425
|
+
if (batch && batch.retry !== retry) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
230
428
|
this.stopReadingHistorical();
|
|
231
429
|
this.emit(EVENTS.ERROR, e);
|
|
232
430
|
});
|
|
@@ -239,9 +437,16 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
239
437
|
const dataHandler = (data) => {
|
|
240
438
|
const blockHeaders = data.getBlockHeaders();
|
|
241
439
|
|
|
242
|
-
if (blockHeaders) {
|
|
243
|
-
const
|
|
244
|
-
|
|
440
|
+
if (blockHeaders && isCurrent()) {
|
|
441
|
+
const rawHeaders = blockHeaders.getHeadersList();
|
|
442
|
+
if (headersObtained + rawHeaders.length > count) {
|
|
443
|
+
// Never take more than was asked for: the rest would wait in memory
|
|
444
|
+
errorHandler(new Error(`Stream sent more than the ${count} headers asked for`));
|
|
445
|
+
this.cancelStream(stream);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const headersList = rawHeaders.map((header) => new BlockHeader(Buffer.from(header)));
|
|
245
450
|
|
|
246
451
|
let rejected = false;
|
|
247
452
|
|
|
@@ -251,10 +456,9 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
251
456
|
*/
|
|
252
457
|
const rejectHeaders = (e) => {
|
|
253
458
|
rejected = true;
|
|
254
|
-
//
|
|
255
|
-
// because they are going th be re-created in errorHandler
|
|
256
|
-
this.cancelStream(stream);
|
|
459
|
+
// Retry first, which detaches the stream, then cancel it
|
|
257
460
|
errorHandler(e);
|
|
461
|
+
this.cancelStream(stream);
|
|
258
462
|
};
|
|
259
463
|
|
|
260
464
|
const batchHeadHeight = fromBlockHeight + headersObtained;
|
|
@@ -265,15 +469,27 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
265
469
|
|
|
266
470
|
if (!rejected) {
|
|
267
471
|
headersObtained += headersList.length;
|
|
472
|
+
|
|
473
|
+
// Done without waiting for the server to end the stream: close it
|
|
474
|
+
// before releasing the batch's slot, so a server that never ends
|
|
475
|
+
// its streams can't keep more than the window open
|
|
476
|
+
if (headersObtained === count) {
|
|
477
|
+
finished = true;
|
|
478
|
+
this.cancelStream(stream);
|
|
479
|
+
this.historicalBatchEnded(batch, stream);
|
|
480
|
+
}
|
|
268
481
|
}
|
|
269
482
|
}
|
|
270
483
|
};
|
|
271
484
|
|
|
272
485
|
const endHandler = () => {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
486
|
+
if (!isCurrent()) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (headersObtained >= count) {
|
|
490
|
+
this.historicalBatchEnded(batch, stream);
|
|
491
|
+
} else {
|
|
492
|
+
errorHandler(new Error(`Stream ended after ${headersObtained} of ${count} headers`));
|
|
277
493
|
}
|
|
278
494
|
};
|
|
279
495
|
|
|
@@ -315,6 +531,10 @@ class BlockHeadersReader extends EventEmitter {
|
|
|
315
531
|
|
|
316
532
|
cancelStream(stream) {
|
|
317
533
|
stream.removeAllListeners();
|
|
534
|
+
// The CANCELLED error a cancelled stream emits must not go unheard
|
|
535
|
+
if (typeof stream.listenerCount === 'function' && stream.listenerCount('error') === 0) {
|
|
536
|
+
stream.on('error', () => {});
|
|
537
|
+
}
|
|
318
538
|
this.cancelStreamTransport(stream);
|
|
319
539
|
}
|
|
320
540
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const sample = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
2
|
-
const networks = require('@dashevo/dashcore-lib/lib/networks');
|
|
3
2
|
|
|
4
3
|
class ListDAPIAddressProvider {
|
|
5
4
|
/**
|
|
@@ -20,28 +19,7 @@ class ListDAPIAddressProvider {
|
|
|
20
19
|
* @returns {Promise<DAPIAddress|undefined>}
|
|
21
20
|
*/
|
|
22
21
|
async getLiveAddress() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const liveAddress = sample(liveAddresses);
|
|
26
|
-
|
|
27
|
-
if (liveAddress === undefined) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// This is a temporary fix for a localhost masternode.
|
|
32
|
-
// On macOS, internal docker IP is used to register masternode, and it's
|
|
33
|
-
// not really possible to bind to that address, so that workaround is introduced.
|
|
34
|
-
const network = networks.get(this.options.network);
|
|
35
|
-
if (network && network.regtestEnabled) {
|
|
36
|
-
const randomNodeIndex = Math.floor(Math.random() * liveAddresses.length);
|
|
37
|
-
|
|
38
|
-
liveAddress.protocol = 'https';
|
|
39
|
-
liveAddress.host = '127.0.0.1';
|
|
40
|
-
liveAddress.allowSelfSignedCertificate = true;
|
|
41
|
-
liveAddress.port = 2443 + randomNodeIndex * 100;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return liveAddress;
|
|
22
|
+
return sample(this.getLiveAddresses());
|
|
45
23
|
}
|
|
46
24
|
|
|
47
25
|
/**
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const networks = require('@dashevo/dashcore-lib/lib/networks');
|
|
2
|
+
|
|
1
3
|
const DAPIAddress = require('./DAPIAddress');
|
|
2
4
|
|
|
3
5
|
class SimplifiedMasternodeListDAPIAddressProvider {
|
|
@@ -5,9 +7,11 @@ class SimplifiedMasternodeListDAPIAddressProvider {
|
|
|
5
7
|
* @param {SimplifiedMasternodeListProvider} smlProvider
|
|
6
8
|
* @param {ListDAPIAddressProvider} listDAPIAddressProvider
|
|
7
9
|
* @param {DAPIAddress[]} addressWhiteList
|
|
10
|
+
* @param {DAPIClientOptions} [options]
|
|
8
11
|
*/
|
|
9
|
-
constructor(smlProvider, listDAPIAddressProvider, addressWhiteList) {
|
|
12
|
+
constructor(smlProvider, listDAPIAddressProvider, addressWhiteList, options = {}) {
|
|
10
13
|
this.smlProvider = smlProvider;
|
|
14
|
+
this.options = options;
|
|
11
15
|
this.listDAPIAddressProvider = listDAPIAddressProvider;
|
|
12
16
|
this.addressWhiteStrings = addressWhiteList.map((dapiAddress) => dapiAddress.toString());
|
|
13
17
|
}
|
|
@@ -58,6 +62,18 @@ class SimplifiedMasternodeListDAPIAddressProvider {
|
|
|
58
62
|
));
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
// Local (regtest) masternodes register a docker-internal IP that is not
|
|
66
|
+
// reachable from the host, while their gateways are published on the host's
|
|
67
|
+
// loopback at the registered port with a self-signed certificate.
|
|
68
|
+
const network = networks.get(this.options.network);
|
|
69
|
+
if (network && network.regtestEnabled) {
|
|
70
|
+
filteredAddresses.forEach((address) => {
|
|
71
|
+
address.setHost('127.0.0.1');
|
|
72
|
+
// eslint-disable-next-line no-param-reassign
|
|
73
|
+
address.allowSelfSignedCertificate = true;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
this.listDAPIAddressProvider.setAddresses(filteredAddresses);
|
|
62
78
|
|
|
63
79
|
return this.listDAPIAddressProvider.getLiveAddress();
|
package/lib/networkConfigs.js
CHANGED
|
@@ -21,11 +21,11 @@ class BlockHeadersWithChainLocksStreamMock extends EventEmitter {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
emit(event,
|
|
24
|
+
emit(event, ...args) {
|
|
25
25
|
if (event === 'error') {
|
|
26
26
|
this.errored = true;
|
|
27
27
|
}
|
|
28
|
-
super.emit(event,
|
|
28
|
+
super.emit(event, ...args);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
@@ -256,8 +256,9 @@ class ReconnectableStream extends EventEmitter {
|
|
|
256
256
|
newArgs = args;
|
|
257
257
|
};
|
|
258
258
|
|
|
259
|
-
// Emit beforeReconnect event in case parent would want to alter arguments
|
|
260
|
-
|
|
259
|
+
// Emit beforeReconnect event in case parent would want to alter arguments,
|
|
260
|
+
// with the error that caused the retry
|
|
261
|
+
this.emit(EVENTS.BEFORE_RECONNECT, updateArgsBeforeReconnect, e);
|
|
261
262
|
|
|
262
263
|
// Wait before reconnecting
|
|
263
264
|
wait(this.retryOnErrorDelay)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashevo/dapi-client",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.1.0-dev.1",
|
|
4
4
|
"description": "Client library used to access Dash DAPI endpoints",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"contributors": [
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
}
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@dashevo/dapi-grpc": "5.0.
|
|
30
|
-
"@dashevo/dash-spv": "6.0.
|
|
29
|
+
"@dashevo/dapi-grpc": "5.1.0-dev.1",
|
|
30
|
+
"@dashevo/dash-spv": "6.1.0-dev.1",
|
|
31
31
|
"@dashevo/dashcore-lib": "~0.26.1",
|
|
32
|
-
"@dashevo/grpc-common": "5.0.
|
|
33
|
-
"@dashevo/wasm-dpp": "5.0.
|
|
32
|
+
"@dashevo/grpc-common": "5.1.0-dev.1",
|
|
33
|
+
"@dashevo/wasm-dpp": "5.1.0-dev.1",
|
|
34
34
|
"cbor": "^8.0.0",
|
|
35
35
|
"google-protobuf": "^3.12.2",
|
|
36
36
|
"undici": "^6.28.1",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"webpack-cli": "^4.9.1"
|
|
73
73
|
},
|
|
74
74
|
"engines": {
|
|
75
|
-
"node": ">=
|
|
75
|
+
"node": ">=22"
|
|
76
76
|
},
|
|
77
77
|
"files": [
|
|
78
78
|
"docs",
|