@dashevo/dash-spv 5.0.0-rc.2 → 5.1.0-beta.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/.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":"73201c7fec9c62f5f87dba7b2d0d54c08fa4fa6f","lib/errors/SPVError.js":"08b7af972c146bcdf29ffaa06eb90ca2c5660914","lib/merkleproofs.js":"dca09952c352109795af64f9490209a96e483e2f","lib/spvchain.js":"4ada79dda7b544debbfc023c1ce5ad058bb0c034","lib/test/bootstrap.js":"e2b2b2b17a9bf0ebff7721d8cb1e5cb75efb0f0f","lib/utils.js":"c2119b88a67698053e783f09c038a04ae208f232","lib/x11.js":"d85e555bad579a8b3e2e92447331933ce9b0c4b1","package.json":"16d4e295753e634788fe6fa947367cc853425a48","test/data/headers.js":"eb9cae85ee11027338fbf1f69e373d22388286d6","test/data/merkleproofs.js":"49ea4c29fb1f0cf6378ed7ee46b0a1db3bb555c1","test/data/rawHeaders.js":"03fed6ca2f8c2d1b63cfa13743431a2d551c7ad9","test/index.js":"e22d4a2ae7ebd44441559a3b728c4fbbb545a68d","test/spvchain.js":"58a7716c91830e0a23bd27ab857cbba70b8bee93"},"deps":{}}
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":"439d9efd858bfa74edaf23fc7fb43cbf37b4af82","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
- // const { hasValidTarget } = require('@dashevo/dark-gravity-wave');
1
+ const { hasValidTarget } = require('@dashevo/dark-gravity-wave');
2
+ const DashUtil = require('@dashevo/dash-util');
2
3
  const merkleProofs = require('./merkleproofs');
3
- // const utils = require('./utils');
4
+ const utils = require('./utils');
4
5
 
5
- // const MIN_TIMESTAMP_HEADERS = 11;
6
- // const MIN_DGW_HEADERS = 24;
6
+ const MIN_TIMESTAMP_HEADERS = 11;
7
+ const MIN_DGW_HEADERS = 24;
7
8
 
8
- // function getMedianTimestamp(headers) {
9
- // const timestamps = headers.map((h) => h.time);
10
- // const median = (arr) => {
11
- // const mid = Math.floor(arr.length / 2);
12
- // const nums = [...arr].sort((a, b) => a - b);
13
- // return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2;
14
- // };
15
- // return median(timestamps);
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
- // function hasGreaterThanMedianTimestamp(newHeader, previousHeaders) {
21
- // if (previousHeaders.length < MIN_TIMESTAMP_HEADERS) return true;
22
- // const headerNormalised = utils.normalizeHeader(newHeader);
23
- // const normalizedLatestHeaders = previousHeaders.slice(
24
- // Math.max(previousHeaders.length - MIN_TIMESTAMP_HEADERS, 0),
25
- // ).map((h) => utils.normalizeHeader(h));
26
- // return getMedianTimestamp(normalizedLatestHeaders) < headerNormalised.time;
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
- function isValidBlockHeader(/* newHeader, previousHeaders, network = 'mainnet' */) {
30
- // TODO: implement consensus checks properly for all networks
31
- return true;
32
- // if (previousHeaders.length > MIN_DGW_HEADERS) {
33
- // return newHeader.validProofOfWork()
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
  };
@@ -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
- this.appendHeadersToLongestChain(chunk);
206
- chunk.forEach((header) => {
207
- this.orphansHashes.delete(header.hash);
208
- });
209
- this.orphanChunks.splice(i, 1);
210
- i -= 1;
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
- // Header at pendingStartBlockHeight is found, initialize chain
335
- this.startBlockHeight = this.pendingStartBlockHeight;
336
- this.pendingStartBlockHeight = null;
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
- const allValid = normalizedHeaders.reduce((acc, header, index, array) => {
346
- const previousHeaders = normalizedHeaders.slice(0, index);
347
- if (index !== 0) {
348
- if (!SpvChain.isParentChild(header, array[index - 1])) {
349
- throw new SPVError(`SPV: Header ${header.hash} is not a child of ${array[index - 1].hash}`);
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
- if (!this.isValid(header, previousHeaders)) {
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
- return acc && true;
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) === -1;
44
+ return hash.compare(target) <= 0;
46
45
  },
47
46
  createBlock(prev, bits) {
48
47
  let i = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashevo/dash-spv",
3
- "version": "5.0.0-rc.2",
3
+ "version": "5.1.0-beta.1",
4
4
  "description": "Repository containing SPV functions used by @dashevo",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- // TODO: restore when conesnsus rules are enabled
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.equal('SPV: Header 00000ce430de949c85a145b02e33ebbaed3772dc8f3d668f66edc6852c24d002 is not a child of e59b97d3d11b4d4563b557e32c62afbdb6e947e9cbb4915c94073f08f7936d5f');
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.equal('Some headers are invalid');
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 and not add invalid but consistent headers', () => {
283
- chain.addHeaders([badRawHeaders[0], badRawHeaders[1]]);
284
- chain.getOrphanChunks().length.should.equal(1);
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.equal('Some headers are invalid');
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