@dashevo/dash-spv 5.0.0 → 5.1.0-beta.2
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/.ultra.cache.json +1 -1
- package/lib/consensus.js +73 -36
- package/lib/merkleproofs.js +1 -1
- package/lib/spvchain.js +57 -35
- package/lib/utils.js +1 -2
- package/package.json +1 -1
- package/test/index.js +40 -8
- package/test/spvchain.js +7 -0
package/.ultra.cache.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"files":{".gitignore":"095d365a56f423d4fab2345c95c137e7c6ea45c0",".mocharc.yml":"f6d924f143da5bc22530e55dcec6922aff69d8e1","LICENSE":"95bf3c7cb642fb61c1bbbb319fd3c83e47a5af96","README.md":"3659fd1bfb44b5e6916679ae586ccbff18683e59","config/config.js":"267c3536b5fe54c88080a56241c34d7393a54ac2","eslint.config.mjs":"cdf50e57d0d206e17f37b855643f55f73717fcd2","index.js":"4413accedb71ffb73ea7ce4ad45a2035c2ce92ca","lib/blockstore.js":"1ed82a8c1cfb4bc3aa96f0669a27c94d2e76b164","lib/consensus.js":"
|
|
1
|
+
{"files":{".gitignore":"095d365a56f423d4fab2345c95c137e7c6ea45c0",".mocharc.yml":"f6d924f143da5bc22530e55dcec6922aff69d8e1","LICENSE":"95bf3c7cb642fb61c1bbbb319fd3c83e47a5af96","README.md":"3659fd1bfb44b5e6916679ae586ccbff18683e59","config/config.js":"267c3536b5fe54c88080a56241c34d7393a54ac2","eslint.config.mjs":"cdf50e57d0d206e17f37b855643f55f73717fcd2","index.js":"4413accedb71ffb73ea7ce4ad45a2035c2ce92ca","lib/blockstore.js":"1ed82a8c1cfb4bc3aa96f0669a27c94d2e76b164","lib/consensus.js":"28c3963e8d70d824ed8d6af8fef9062e35f6273b","lib/errors/SPVError.js":"08b7af972c146bcdf29ffaa06eb90ca2c5660914","lib/merkleproofs.js":"1bba2c774e52e3cdfd4b99973c11229e6cf2cf6d","lib/spvchain.js":"314adc7f2e810bcceb5f30f4c90b6e566cf7650a","lib/test/bootstrap.js":"e2b2b2b17a9bf0ebff7721d8cb1e5cb75efb0f0f","lib/utils.js":"4ca99de11dd84867be29f1bab1ef09000fa8b5df","lib/x11.js":"d85e555bad579a8b3e2e92447331933ce9b0c4b1","package.json":"362222731d5a0279c538d1f7ef3495d023419485","test/data/headers.js":"eb9cae85ee11027338fbf1f69e373d22388286d6","test/data/merkleproofs.js":"49ea4c29fb1f0cf6378ed7ee46b0a1db3bb555c1","test/data/rawHeaders.js":"03fed6ca2f8c2d1b63cfa13743431a2d551c7ad9","test/index.js":"1a15df18c1354fcea8015947ed43ac89ab1e7c25","test/spvchain.js":"f6441870e91b7faf2e6b23d034f40d007a697652"},"deps":{}}
|
package/lib/consensus.js
CHANGED
|
@@ -1,46 +1,82 @@
|
|
|
1
|
-
|
|
1
|
+
const { hasValidTarget } = require('@dashevo/dark-gravity-wave');
|
|
2
|
+
const DashUtil = require('@dashevo/dash-util');
|
|
2
3
|
const merkleProofs = require('./merkleproofs');
|
|
3
|
-
|
|
4
|
+
const utils = require('./utils');
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
const MIN_TIMESTAMP_HEADERS = 11;
|
|
7
|
+
const MIN_DGW_HEADERS = 24;
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
const NETWORK_POW_LIMITS = {
|
|
10
|
+
mainnet: 0x1e0ffff0,
|
|
11
|
+
testnet: 0x1e0fffff,
|
|
12
|
+
devnet: 0x207fffff,
|
|
13
|
+
regtest: 0x207fffff,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function getMedianTimestamp(headers) {
|
|
17
|
+
const timestamps = headers.map((h) => h.time).sort((a, b) => a - b);
|
|
18
|
+
return timestamps[Math.floor(timestamps.length / 2)];
|
|
19
|
+
}
|
|
17
20
|
|
|
18
21
|
// Must be strictly greater than the median time of the previous 11 blocks.
|
|
19
22
|
// https://dash-docs.github.io/en/developer-reference#block-headers
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
function hasGreaterThanMedianTimestamp(newHeader, previousHeaders) {
|
|
24
|
+
if (previousHeaders.length < MIN_TIMESTAMP_HEADERS) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
const normalizedHeader = utils.normalizeHeader(newHeader);
|
|
28
|
+
const latestHeaders = previousHeaders
|
|
29
|
+
.slice(-MIN_TIMESTAMP_HEADERS)
|
|
30
|
+
.map((header) => utils.normalizeHeader(header));
|
|
31
|
+
return getMedianTimestamp(latestHeaders) < normalizedHeader.time;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hasCanonicalTarget(header, network) {
|
|
35
|
+
const powLimit = NETWORK_POW_LIMITS[network];
|
|
36
|
+
if (!powLimit || !Number.isInteger(header.bits)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const target = DashUtil.expandTarget(header.bits);
|
|
42
|
+
const canonicalBits = DashUtil.compressTarget(target);
|
|
43
|
+
const maximumTarget = DashUtil.expandTarget(powLimit);
|
|
44
|
+
|
|
45
|
+
return canonicalBits === header.bits && target.compare(maximumTarget) <= 0;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isValidBlockHeaderWithoutContext(newHeader, network = 'mainnet') {
|
|
52
|
+
const normalizedHeader = utils.normalizeHeader(newHeader);
|
|
53
|
+
|
|
54
|
+
return hasCanonicalTarget(normalizedHeader, network)
|
|
55
|
+
&& utils.validProofOfWork(normalizedHeader)
|
|
56
|
+
&& normalizedHeader.validTimestamp();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isValidBlockHeader(newHeader, previousHeaders, network = 'mainnet') {
|
|
60
|
+
const normalizedHeader = utils.normalizeHeader(newHeader);
|
|
61
|
+
const normalizedPreviousHeaders = previousHeaders.map((header) => utils.normalizeHeader(header));
|
|
62
|
+
|
|
63
|
+
if (!isValidBlockHeaderWithoutContext(normalizedHeader, network)
|
|
64
|
+
|| !hasGreaterThanMedianTimestamp(normalizedHeader, normalizedPreviousHeaders)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A trusted checkpoint may contain fewer than a full DGW window. Proof of
|
|
69
|
+
// work, the network pow limit, and timestamp rules still apply immediately;
|
|
70
|
+
// exact difficulty validation begins as soon as the required history exists.
|
|
71
|
+
if (normalizedPreviousHeaders.length < MIN_DGW_HEADERS) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
28
74
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// && newHeader.validTimestamp()
|
|
35
|
-
// && hasGreaterThanMedianTimestamp(newHeader, previousHeaders)
|
|
36
|
-
// && hasValidTarget(
|
|
37
|
-
// utils.getDgwBlock(newHeader), previousHeaders.map((h) => utils.getDgwBlock(h)), network,
|
|
38
|
-
// );
|
|
39
|
-
// }
|
|
40
|
-
// // TODO: eventually this check start failing in dashmate
|
|
41
|
-
// return newHeader.validProofOfWork()
|
|
42
|
-
// && newHeader.validTimestamp()
|
|
43
|
-
// && hasGreaterThanMedianTimestamp(newHeader, previousHeaders);
|
|
75
|
+
return hasValidTarget(
|
|
76
|
+
utils.getDgwBlock(normalizedHeader),
|
|
77
|
+
normalizedPreviousHeaders.map((header) => utils.getDgwBlock(header)),
|
|
78
|
+
network,
|
|
79
|
+
);
|
|
44
80
|
}
|
|
45
81
|
|
|
46
82
|
/**
|
|
@@ -64,5 +100,6 @@ async function areValidTransactions(transactions, merkleBlock, headerChain) {
|
|
|
64
100
|
|
|
65
101
|
module.exports = {
|
|
66
102
|
isValidBlockHeader,
|
|
103
|
+
isValidBlockHeaderWithoutContext,
|
|
67
104
|
areValidTransactions,
|
|
68
105
|
};
|
package/lib/merkleproofs.js
CHANGED
|
@@ -13,7 +13,7 @@ const merkleproofs = {
|
|
|
13
13
|
if (typeof transactions[0] === 'string') {
|
|
14
14
|
txToFilter = txToFilter.map((tx) => DashUtil.toHash(tx).toString('hex'));
|
|
15
15
|
}
|
|
16
|
-
return merkleBlock.validMerkleTree
|
|
16
|
+
return merkleBlock.validMerkleTree()
|
|
17
17
|
&& txToFilter.filter((tx) => merkleBlock.hasTransaction(tx)).length === transactions.length;
|
|
18
18
|
},
|
|
19
19
|
};
|
package/lib/spvchain.js
CHANGED
|
@@ -202,16 +202,53 @@ const SpvChain = class {
|
|
|
202
202
|
const chunkPrevHash = utils.getCorrectedHash(chunk[0].prevHash);
|
|
203
203
|
|
|
204
204
|
if (tipHash === chunkPrevHash) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
this.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
try {
|
|
206
|
+
this.validateConnectedHeaders(chunk, this.getValidationHistory(), true);
|
|
207
|
+
this.appendHeadersToLongestChain(chunk);
|
|
208
|
+
chunk.forEach((header) => {
|
|
209
|
+
this.orphansHashes.delete(header.hash);
|
|
210
|
+
});
|
|
211
|
+
this.orphanChunks.splice(i, 1);
|
|
212
|
+
i -= 1;
|
|
213
|
+
} catch (e) {
|
|
214
|
+
chunk.forEach((header) => {
|
|
215
|
+
this.orphansHashes.delete(header.hash);
|
|
216
|
+
});
|
|
217
|
+
this.orphanChunks.splice(i, 1);
|
|
218
|
+
throw e;
|
|
219
|
+
}
|
|
211
220
|
}
|
|
212
221
|
}
|
|
213
222
|
}
|
|
214
223
|
|
|
224
|
+
/** @private */
|
|
225
|
+
getValidationHistory() {
|
|
226
|
+
return this.prunedHeaders.concat(this.getLongestChain()).slice(-24);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @private */
|
|
230
|
+
validateConnectedHeaders(headers, initialHistory, allowKnownOrphans = false) {
|
|
231
|
+
const history = initialHistory.slice();
|
|
232
|
+
|
|
233
|
+
headers.forEach((header) => {
|
|
234
|
+
const previousHeader = history[history.length - 1];
|
|
235
|
+
if (!previousHeader || !SpvChain.isParentChild(header, previousHeader)) {
|
|
236
|
+
throw new SPVError(`SPV: Header ${header.hash} is not connected to verified history`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const isKnown = this.heightByHash.has(header.hash)
|
|
240
|
+
|| (!allowKnownOrphans && this.orphansHashes.has(header.hash));
|
|
241
|
+
if (isKnown || !Consensus.isValidBlockHeader(header, history, this.network)) {
|
|
242
|
+
throw new SPVError(`SPV: Header ${header.hash} is invalid`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
history.push(header);
|
|
246
|
+
if (history.length > 24) {
|
|
247
|
+
history.shift();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
215
252
|
/** @private */
|
|
216
253
|
getOrphanChunks() {
|
|
217
254
|
return this.orphanChunks;
|
|
@@ -230,7 +267,6 @@ const SpvChain = class {
|
|
|
230
267
|
return !!validBlockHeader && !duplicate;
|
|
231
268
|
}
|
|
232
269
|
|
|
233
|
-
/* eslint-disable no-param-reassign */
|
|
234
270
|
/**
|
|
235
271
|
* verifies the parent child connection
|
|
236
272
|
* between two adjacent dashcore.BlockHeader objects
|
|
@@ -331,10 +367,9 @@ const SpvChain = class {
|
|
|
331
367
|
|
|
332
368
|
isOrphan = !SpvChain.isParentChild(firstHeader, tip);
|
|
333
369
|
} else if (batchHeadHeight === this.pendingStartBlockHeight) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
isOrphan = false;
|
|
370
|
+
throw new SPVError(
|
|
371
|
+
'SPV chain cannot initialize from an unauthenticated remote checkpoint',
|
|
372
|
+
);
|
|
338
373
|
} else if (batchHeadHeight > this.pendingStartBlockHeight) {
|
|
339
374
|
// Orphan chunk has arrived
|
|
340
375
|
isOrphan = true;
|
|
@@ -342,38 +377,25 @@ const SpvChain = class {
|
|
|
342
377
|
throw new SPVError(`Batch at invalid height arrived: ${batchHeadHeight}, expected > ${this.pendingStartBlockHeight}`);
|
|
343
378
|
}
|
|
344
379
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
380
|
+
if (isOrphan) {
|
|
381
|
+
normalizedHeaders.forEach((header, index) => {
|
|
382
|
+
if (index > 0 && !SpvChain.isParentChild(header, normalizedHeaders[index - 1])) {
|
|
383
|
+
throw new SPVError(
|
|
384
|
+
`SPV: Header ${header.hash} is not a child of ${normalizedHeaders[index - 1].hash}`,
|
|
385
|
+
);
|
|
350
386
|
}
|
|
351
|
-
|
|
352
|
-
|
|
387
|
+
if (this.isDuplicate(header.hash)
|
|
388
|
+
|| !Consensus.isValidBlockHeaderWithoutContext(header, this.network)) {
|
|
353
389
|
throw new SPVError(`SPV: Header ${header.hash} is invalid`);
|
|
354
390
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (isOrphan) {
|
|
358
|
-
if (!this.isValid(header, previousHeaders)) {
|
|
359
|
-
throw new SPVError('Some headers are invalid');
|
|
360
|
-
}
|
|
361
|
-
return acc && true;
|
|
362
|
-
}
|
|
363
|
-
if (!this.isValid(header, this.getLongestChain())) {
|
|
364
|
-
throw new SPVError('Some headers are invalid');
|
|
365
|
-
}
|
|
366
|
-
return acc && true;
|
|
367
|
-
}, true);
|
|
368
|
-
if (!allValid) {
|
|
369
|
-
throw new SPVError('Some headers are invalid');
|
|
370
|
-
}
|
|
371
|
-
if (isOrphan) {
|
|
391
|
+
});
|
|
392
|
+
|
|
372
393
|
normalizedHeaders.forEach((header) => {
|
|
373
394
|
this.orphansHashes.add(header.hash);
|
|
374
395
|
});
|
|
375
396
|
this.orphanChunks.push(normalizedHeaders);
|
|
376
397
|
} else {
|
|
398
|
+
this.validateConnectedHeaders(normalizedHeaders, this.getValidationHistory());
|
|
377
399
|
this.appendHeadersToLongestChain(normalizedHeaders);
|
|
378
400
|
}
|
|
379
401
|
if (this.orphanChunks.length > 0) {
|
package/lib/utils.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
/* eslint no-underscore-dangle: ["error", { "allow": ["_getHash"] }] */
|
|
2
|
-
/* eslint-disable no-bitwise */
|
|
3
2
|
const DashUtil = require('@dashevo/dash-util');
|
|
4
3
|
const dashcore = require('@dashevo/dashcore-lib');
|
|
5
4
|
|
|
@@ -42,7 +41,7 @@ module.exports = {
|
|
|
42
41
|
validProofOfWork(header) {
|
|
43
42
|
const target = DashUtil.expandTarget(header.bits);
|
|
44
43
|
const hash = header._getHash().reverse();
|
|
45
|
-
return hash.compare(target)
|
|
44
|
+
return hash.compare(target) <= 0;
|
|
46
45
|
},
|
|
47
46
|
createBlock(prev, bits) {
|
|
48
47
|
let i = 0;
|
package/package.json
CHANGED
package/test/index.js
CHANGED
|
@@ -67,8 +67,7 @@ describe('SPV-DASH (forks & re-orgs) deserialized headers', () => {
|
|
|
67
67
|
chain.getLongestChain().length.should.equal(25);
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
it.skip('not add an invalid header', () => {
|
|
70
|
+
it('should not add an invalid header', () => {
|
|
72
71
|
should(() => chain.addHeaders([headers[25]])).throw();
|
|
73
72
|
chain.getLongestChain().length.should.equal(25);
|
|
74
73
|
});
|
|
@@ -78,7 +77,7 @@ describe('SPV-DASH (forks & re-orgs) deserialized headers', () => {
|
|
|
78
77
|
chain.addHeaders([headers[25], headers[10]]);
|
|
79
78
|
done(new Error('SPV chain failed to throw an error on invalid block'));
|
|
80
79
|
} catch (e) {
|
|
81
|
-
e.message.should.
|
|
80
|
+
e.message.should.match(/SPV: Header .* (is invalid|is not a child of)/);
|
|
82
81
|
done();
|
|
83
82
|
}
|
|
84
83
|
});
|
|
@@ -129,6 +128,39 @@ describe('SPV-DASH (forks & re-orgs) serialized raw headers for mainnet', () =>
|
|
|
129
128
|
});
|
|
130
129
|
});
|
|
131
130
|
|
|
131
|
+
describe('Block header consensus validation', () => {
|
|
132
|
+
before(async () => {
|
|
133
|
+
await Blockchain.wasmX11Ready();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should reject a linked header that does not satisfy proof of work', () => {
|
|
137
|
+
const invalidHeader = utils.normalizeHeader(headers[0]);
|
|
138
|
+
invalidHeader.bits = 0;
|
|
139
|
+
|
|
140
|
+
consensus.isValidBlockHeader(
|
|
141
|
+
invalidHeader,
|
|
142
|
+
[new Blockchain('testnet').genesis],
|
|
143
|
+
'testnet',
|
|
144
|
+
).should.equal(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should validate difficulty using history from before the current batch', () => {
|
|
148
|
+
const previousHeaders = mainnet.slice(0, 24).map(utils.normalizeHeader);
|
|
149
|
+
const nextHeader = utils.normalizeHeader(mainnet[24]);
|
|
150
|
+
|
|
151
|
+
consensus.isValidBlockHeader(nextHeader, previousHeaders, 'mainnet')
|
|
152
|
+
.should.equal(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should reject a proof-of-work-valid header for the wrong difficulty history', () => {
|
|
156
|
+
const wrongHistory = testnet.slice(0, 24).map(utils.normalizeHeader);
|
|
157
|
+
const nextHeader = utils.normalizeHeader(mainnet[24]);
|
|
158
|
+
|
|
159
|
+
consensus.isValidBlockHeader(nextHeader, wrongHistory, 'mainnet')
|
|
160
|
+
.should.equal(false);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
132
164
|
describe('SPV-DASH (addHeaders) add many headers for testnet', () => {
|
|
133
165
|
before(async () => {
|
|
134
166
|
chain = new Blockchain('testnet', 10000);
|
|
@@ -197,7 +229,7 @@ describe('SPV-DASH (addHeaders) add many headers for testnet', () => {
|
|
|
197
229
|
chain.addHeaders([badRawHeaders[0], badRawHeaders[2]]);
|
|
198
230
|
done(new Error('SPV chain failed to throw an error on invalid block'));
|
|
199
231
|
} catch (e) {
|
|
200
|
-
e.message.should.
|
|
232
|
+
e.message.should.match(/SPV: Header .* (is invalid|is not a child of)/);
|
|
201
233
|
done();
|
|
202
234
|
}
|
|
203
235
|
});
|
|
@@ -279,9 +311,9 @@ describe('SPV-DASH (addHeaders) add many headers for mainnet', () => {
|
|
|
279
311
|
chain.getLongestChain().length.should.equal(1500);
|
|
280
312
|
});
|
|
281
313
|
|
|
282
|
-
it('should orphan
|
|
283
|
-
chain.addHeaders([badRawHeaders[0], badRawHeaders[1]]);
|
|
284
|
-
chain.getOrphanChunks().length.should.equal(
|
|
314
|
+
it('should reject orphan headers outside the network proof-of-work limit', () => {
|
|
315
|
+
should(() => chain.addHeaders([badRawHeaders[0], badRawHeaders[1]])).throw();
|
|
316
|
+
chain.getOrphanChunks().length.should.equal(0);
|
|
285
317
|
chain.getLongestChain().length.should.equal(1500);
|
|
286
318
|
});
|
|
287
319
|
|
|
@@ -290,7 +322,7 @@ describe('SPV-DASH (addHeaders) add many headers for mainnet', () => {
|
|
|
290
322
|
chain.addHeaders([badRawHeaders[0], badRawHeaders[2]]);
|
|
291
323
|
done(new Error('SPV chain failed to throw an error on invalid block'));
|
|
292
324
|
} catch (e) {
|
|
293
|
-
e.message.should.
|
|
325
|
+
e.message.should.match(/SPV: Header .* (is invalid|is not a child of)/);
|
|
294
326
|
done();
|
|
295
327
|
}
|
|
296
328
|
});
|
package/test/spvchain.js
CHANGED
|
@@ -56,6 +56,13 @@ describe('SPVChain', () => {
|
|
|
56
56
|
.to.throw('Chain not initialized, either call initialize() or set pendingStartBlockHeight');
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
it('should reject an unauthenticated remote bootstrap header', () => {
|
|
60
|
+
spvChain.pendingStartBlockHeight = 10400;
|
|
61
|
+
|
|
62
|
+
expect(() => spvChain.addHeaders(testnet.slice(400), 10400))
|
|
63
|
+
.to.throw('SPV chain cannot initialize from an unauthenticated remote checkpoint');
|
|
64
|
+
});
|
|
65
|
+
|
|
59
66
|
it('should assemble headers chain if headers arriving out of order', async () => {
|
|
60
67
|
expect(true).to.equal(true);
|
|
61
68
|
|