@dashevo/dash-spv 0.24.0-dev.6 → 0.25.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/.ultra.cache.json CHANGED
@@ -1 +1 @@
1
- {"files":{".eslintignore":"5772aab4a649e8c99c5b74bd685792cd6a69f9ac",".eslintrc.json":"41aa1d3c1477f203b588dcb5c0015454edd94481",".gitignore":"095d365a56f423d4fab2345c95c137e7c6ea45c0","LICENSE":"95bf3c7cb642fb61c1bbbb319fd3c83e47a5af96","README.md":"3659fd1bfb44b5e6916679ae586ccbff18683e59","config/config.js":"336469b011520eb7ac5b57a3dc439191f2ae3133","index.js":"fefb01f324a8ffbd9f9354c709e8e880eca6f261","lib/blockstore.js":"1ed82a8c1cfb4bc3aa96f0669a27c94d2e76b164","lib/consensus.js":"8ef5fefd0476056e108351de8edfbe8e60e89105","lib/merkleproofs.js":"dca09952c352109795af64f9490209a96e483e2f","lib/spvchain.js":"7f90590769100e132802042ca9f0982297ab242c","lib/utils.js":"c2119b88a67698053e783f09c038a04ae208f232","package.json":"965eab815953776b7a07aede2c73e31e31712864","test/data/headers.js":"eb9cae85ee11027338fbf1f69e373d22388286d6","test/data/merkleproofs.js":"49ea4c29fb1f0cf6378ed7ee46b0a1db3bb555c1","test/data/rawHeaders.js":"03fed6ca2f8c2d1b63cfa13743431a2d551c7ad9","test/index.js":"de1a2b779318fc1d8b66c843baf4e758b0bd805d"},"deps":{}}
1
+ {"files":{".eslintignore":"5772aab4a649e8c99c5b74bd685792cd6a69f9ac",".eslintrc.json":"41aa1d3c1477f203b588dcb5c0015454edd94481",".gitignore":"095d365a56f423d4fab2345c95c137e7c6ea45c0","LICENSE":"95bf3c7cb642fb61c1bbbb319fd3c83e47a5af96","README.md":"3659fd1bfb44b5e6916679ae586ccbff18683e59","config/config.js":"267c3536b5fe54c88080a56241c34d7393a54ac2","index.js":"4413accedb71ffb73ea7ce4ad45a2035c2ce92ca","lib/blockstore.js":"1ed82a8c1cfb4bc3aa96f0669a27c94d2e76b164","lib/consensus.js":"73201c7fec9c62f5f87dba7b2d0d54c08fa4fa6f","lib/errors/SPVError.js":"08b7af972c146bcdf29ffaa06eb90ca2c5660914","lib/merkleproofs.js":"dca09952c352109795af64f9490209a96e483e2f","lib/spvchain.js":"dfb38386a8c0eb1fee0c64e1b1a950125811f6db","lib/utils.js":"c2119b88a67698053e783f09c038a04ae208f232","package.json":"42f7493a9e59d0411fc862f3160f95d3bb526024","test/data/headers.js":"eb9cae85ee11027338fbf1f69e373d22388286d6","test/data/merkleproofs.js":"49ea4c29fb1f0cf6378ed7ee46b0a1db3bb555c1","test/data/rawHeaders.js":"03fed6ca2f8c2d1b63cfa13743431a2d551c7ad9","test/index.js":"e937184e5ee21ba882e09fcd5e5f5228d9b7ef68","test/spvchain.js":"7e6c93b2e623a3ddb93e9f250133e83f28c882e2"},"deps":{}}
package/config/config.js CHANGED
@@ -30,9 +30,9 @@ module.exports = {
30
30
  version: 1,
31
31
  previousblockhash: '0000000000000000000000000000000000000000000000000000000000000000',
32
32
  merkleroot: 'e0028eb9648db56b1ac77cf090b99048a8007e2bb64b68f092c03c7f56a662c7',
33
- time: 1390666206,
34
- bits: '1e0ffff0',
35
- nonce: 3861367235,
33
+ time: 1417713337,
34
+ bits: '207fffff',
35
+ nonce: 1096447,
36
36
  });
37
37
  },
38
38
  getRegtestGenesis() {
@@ -40,12 +40,19 @@ module.exports = {
40
40
  version: 1,
41
41
  previousblockhash: '0000000000000000000000000000000000000000000000000000000000000000',
42
42
  merkleroot: 'e0028eb9648db56b1ac77cf090b99048a8007e2bb64b68f092c03c7f56a662c7',
43
- time: 1390666206,
44
- bits: '1e0ffff0',
45
- nonce: 3861367235,
43
+ time: 1417713337,
44
+ bits: '207fffff',
45
+ nonce: 1096447,
46
46
  });
47
47
  },
48
48
  getLivenetGenesis() {
49
- throw Error('Livenet genesis not yet implemented');
49
+ return utils.normalizeHeader({
50
+ version: 1,
51
+ previousblockhash: '0000000000000000000000000000000000000000000000000000000000000000',
52
+ merkleroot: 'e0028eb9648db56b1ac77cf090b99048a8007e2bb64b68f092c03c7f56a662c7',
53
+ time: 1390095618,
54
+ bits: '1e0ffff0',
55
+ nonce: 28917698,
56
+ });
50
57
  },
51
58
  };
package/index.js CHANGED
@@ -1,7 +1,11 @@
1
1
  const spvChain = require('./lib/spvchain');
2
2
  const merkleProof = require('./lib/merkleproofs');
3
+ const genesis = require('./config/config');
4
+ const SPVError = require('./lib/errors/SPVError');
3
5
 
4
6
  module.exports = {
5
7
  SpvChain: spvChain,
6
8
  MerkleProof: merkleProof,
9
+ genesis,
10
+ SPVError,
7
11
  };
package/lib/consensus.js CHANGED
@@ -1,43 +1,46 @@
1
- const { hasValidTarget } = require('@dashevo/dark-gravity-wave');
1
+ // const { hasValidTarget } = require('@dashevo/dark-gravity-wave');
2
2
  const merkleProofs = require('./merkleproofs');
3
- const utils = require('./utils');
3
+ // const utils = require('./utils');
4
4
 
5
- const MIN_TIMESTAMP_HEADERS = 11;
6
- const MIN_DGW_HEADERS = 24;
5
+ // const MIN_TIMESTAMP_HEADERS = 11;
6
+ // const MIN_DGW_HEADERS = 24;
7
7
 
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
- }
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
+ // }
17
17
 
18
18
  // Must be strictly greater than the median time of the previous 11 blocks.
19
19
  // 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
- }
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
+ // }
28
28
 
29
- function isValidBlockHeader(newHeader, previousHeaders, network = 'mainnet') {
30
- if (previousHeaders.length > MIN_DGW_HEADERS) {
31
- return newHeader.validProofOfWork()
32
- && newHeader.validTimestamp()
33
- && hasGreaterThanMedianTimestamp(newHeader, previousHeaders)
34
- && hasValidTarget(
35
- utils.getDgwBlock(newHeader), previousHeaders.map((h) => utils.getDgwBlock(h)), network,
36
- );
37
- }
38
- return newHeader.validProofOfWork()
39
- && newHeader.validTimestamp()
40
- && hasGreaterThanMedianTimestamp(newHeader, previousHeaders);
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);
41
44
  }
42
45
 
43
46
  /**
@@ -0,0 +1,8 @@
1
+ class SPVError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'SPVError';
5
+ }
6
+ }
7
+
8
+ module.exports = SPVError;
package/lib/spvchain.js CHANGED
@@ -1,17 +1,34 @@
1
- const BlockStore = require('./blockstore');
2
1
  const config = require('../config/config');
3
2
  const Consensus = require('./consensus');
4
3
  const utils = require('./utils');
4
+ const SPVError = require('./errors/SPVError');
5
5
 
6
6
  const SpvChain = class {
7
- constructor(chainType, confirms = 100, startBlock) {
8
- this.root = null;
9
- this.allBranches = [];
10
- this.orphanBlocks = [];
11
- this.orphanChunks = [];
7
+ // TODO: move chainType and confirms to `options`
8
+ constructor(chainType, confirms = 100, startBlock, startBlockHeight) {
12
9
  this.confirmsBeforeFinal = confirms;
10
+
11
+ this.reset(startBlockHeight);
13
12
  this.init(chainType, startBlock);
14
- this.store = new BlockStore();
13
+
14
+ this.hashesByHeight = new Map([[this.startBlockHeight, this.root.hash]]);
15
+
16
+ this.heightByHash = new Map([[this.root.hash, this.startBlockHeight]]);
17
+
18
+ // TODO: legacy - remove this
19
+ this.orphanBlocks = [];
20
+ }
21
+
22
+ reset(fromBlockHeight = 0) {
23
+ this.root = null;
24
+ this.allBranches = [[]];
25
+ this.orphanChunks = [];
26
+ this.prunedHeaders = [];
27
+
28
+ this.hashesByHeight = new Map();
29
+ this.heightByHash = new Map();
30
+ this.orphansHashes = new Set();
31
+ this.startBlockHeight = fromBlockHeight < 0 ? 0 : fromBlockHeight;
15
32
  }
16
33
 
17
34
  init(chainType, startBlock) {
@@ -32,6 +49,7 @@ const SpvChain = class {
32
49
  }
33
50
  this.root = config.getDevnetGenesis();
34
51
  break;
52
+ case 'local':
35
53
  case 'regtest':
36
54
  this.network = 'regtest';
37
55
  if (startBlock) {
@@ -69,24 +87,62 @@ const SpvChain = class {
69
87
  this.root = startBlock;
70
88
  this.network = 'mainnet';
71
89
  } else {
72
- throw new Error('Unhandled chaintype or startBlock not provided');
90
+ throw new SPVError('Unhandled chaintype or startBlock not provided');
73
91
  }
74
92
  break;
75
93
  }
76
- this.root.children = [];
94
+ // this.root.children = [];
77
95
  this.setAllBranches();
78
96
  }
79
97
 
98
+ validate() {
99
+ const longestChain = this.getLongestChain();
100
+ if (!longestChain.length) {
101
+ throw new SPVError('Empty SPV chain');
102
+ }
103
+ const head = longestChain[longestChain.length - 1];
104
+ const tail = this.prunedHeaders[0] || longestChain[0];
105
+
106
+ const headHeight = this.heightByHash.get(head.hash);
107
+ const tailHeight = this.heightByHash.get(tail.hash);
108
+
109
+ if (this.orphanChunks.length) {
110
+ throw new SPVError('Chain contains orphan chunks');
111
+ }
112
+
113
+ if (typeof headHeight !== 'number') {
114
+ throw new SPVError(`Head header '${head.hash}' height is invalid ${headHeight}`);
115
+ }
116
+
117
+ if (typeof tailHeight !== 'number') {
118
+ throw new SPVError(`Tail header '${tail.hash}' height is invalid ${tailHeight}`);
119
+ }
120
+
121
+ if (this.hashesByHeight.get(headHeight) !== head.hash) {
122
+ throw new SPVError(`Head header '${head.hash}' height mismatch`);
123
+ }
124
+
125
+ if (this.hashesByHeight.get(tailHeight) !== tail.hash) {
126
+ throw new SPVError(`Tail header '${tail.hash}' height mismatch`);
127
+ }
128
+
129
+ const expectedChainLength = headHeight - tailHeight + 1;
130
+ const actualChainLength = longestChain.length + this.prunedHeaders.length;
131
+
132
+ if (expectedChainLength !== actualChainLength) {
133
+ throw new SPVError(`Chain length mismatch: expected ${expectedChainLength}, actual ${actualChainLength}`);
134
+ }
135
+ }
136
+
80
137
  /** @private */
81
138
  checkPruneBlocks() {
82
139
  const longestChain = this.getLongestChain();
83
140
 
84
- while (longestChain.length > this.confirmsBeforeFinal) {
85
- const pruneBlock = longestChain.splice(0, 1)[0];
86
- // Children discarded as stale branches
87
- delete pruneBlock.orphan;
88
- this.store.put(pruneBlock);
89
- }
141
+ longestChain
142
+ .splice(0, longestChain.length - this.confirmsBeforeFinal)
143
+ .forEach((header) => {
144
+ this.prunedHeaders.push(header);
145
+ });
90
146
  }
91
147
 
92
148
  /** @private */
@@ -97,7 +153,7 @@ const SpvChain = class {
97
153
  if (node.hash === utils.getCorrectedHash(newHeader.prevHash)) {
98
154
  return node;
99
155
  }
100
- node.children.forEach((c) => { stack.push(c); });
156
+ // node.children.forEach((c) => { stack.push(c); });
101
157
  }
102
158
  return null;
103
159
  }
@@ -107,18 +163,29 @@ const SpvChain = class {
107
163
  this.allBranches = [];
108
164
  branch.push(node);
109
165
 
110
- node.children.forEach((c) => {
111
- this.setAllBranches(c, Array.from(branch));
112
- });
166
+ // node.children.forEach((c) => {
167
+ // this.setAllBranches(c, Array.from(branch));
168
+ // });
113
169
 
114
- if (node.children.length === 0) {
115
- this.allBranches.push(branch);
116
- }
170
+ // if (node.children.length === 0) {
171
+ this.allBranches.push(branch);
172
+ // }
117
173
  }
118
174
 
119
175
  /** @private */
120
176
  appendHeadersToLongestChain(headers) {
121
- const newLongestChain = this.getLongestChain().concat(headers);
177
+ const longestChain = this.getLongestChain();
178
+ const lastHeight = this.startBlockHeight
179
+ + longestChain.length + this.prunedHeaders.length;
180
+
181
+ headers.forEach((header, i) => {
182
+ const height = lastHeight + i;
183
+ this.hashesByHeight.set(height, header.hash);
184
+ this.heightByHash.set(header.hash, height);
185
+ });
186
+
187
+ const newLongestChain = longestChain.concat(headers);
188
+
122
189
  this.allBranches = [];
123
190
  this.allBranches.push(newLongestChain);
124
191
  }
@@ -129,38 +196,48 @@ const SpvChain = class {
129
196
  }
130
197
 
131
198
  /** @private */
132
- isDuplicate(compareHash) {
133
- return this.getAllBranches().map((branch) => branch.map((node) => node.hash))
134
- .concat(this.orphanBlocks.map((orphan) => orphan.hash))
135
- .filter((hash) => hash === compareHash).length > 0;
199
+ isDuplicate(hash) {
200
+ return this.heightByHash.has(hash) || this.orphansHashes.has(hash);
136
201
  }
137
202
 
138
203
  /** @private */
139
- orphanReconnect() {
140
- for (let i = 0; i < this.orphanBlocks.length; i += 1) {
141
- const connectionTip = this.findConnection(this.orphanBlocks[i]);
142
- if (connectionTip) {
143
- connectionTip.children.push(this.orphanBlocks[i]);
144
- this.orphanBlocks.splice(i, 1);
145
- }
146
- }
147
- }
204
+ // orphanReconnect() {
205
+ // for (let i = 0; i < this.orphanBlocks.length; i += 1) {
206
+ // const connectionTip = this.findConnection(this.orphanBlocks[i]);
207
+ // if (connectionTip) {
208
+ // connectionTip.children.push(this.orphanBlocks[i]);
209
+ // this.orphanBlocks.splice(i, 1);
210
+ // }
211
+ // }
212
+ // }
148
213
 
149
214
  /** @private */
150
215
  orphanChunksReconnect() {
216
+ // TODO: consider optimizing with map of { [chunkHeadHash]: chunkIndex }
217
+ // to get rid of sorting and make the whole function of O(n) complexity
151
218
  this.orphanChunks.sort((a, b) => a[0].timestamp - b[0].timestamp);
152
- this.orphanChunks.slice().forEach((chunk, index) => {
153
- if (this.getTipHash() === utils.getCorrectedHash(chunk[0].prevHash)) {
219
+
220
+ for (let i = 0; i < this.orphanChunks.length; i += 1) {
221
+ const chunk = this.orphanChunks[i];
222
+ const tipHash = this.getTipHash();
223
+ const chunkPrevHash = utils.getCorrectedHash(chunk[0].prevHash);
224
+
225
+ if (tipHash === chunkPrevHash) {
154
226
  this.appendHeadersToLongestChain(chunk);
155
- this.orphanChunks.splice(index, 1);
227
+ chunk.forEach((header) => {
228
+ this.orphansHashes.delete(header.hash);
229
+ });
230
+ this.orphanChunks.splice(i, 1);
231
+ i -= 1;
156
232
  }
157
- });
233
+ }
158
234
  }
159
235
 
160
236
  /** @private */
161
- getOrphans() {
162
- return this.orphanBlocks;
163
- }
237
+ // TODO: remove
238
+ // getOrphans() {
239
+ // return this.orphanBlocks;
240
+ // }
164
241
 
165
242
  /** @private */
166
243
  getOrphanChunks() {
@@ -168,15 +245,15 @@ const SpvChain = class {
168
245
  }
169
246
 
170
247
  /** @private */
171
- processValidHeader(header) {
172
- const connection = this.findConnection(header);
173
- if (connection) {
174
- connection.children.push(header);
175
- this.orphanReconnect();
176
- } else {
177
- this.orphanBlocks.push(header);
178
- }
179
- }
248
+ // processValidHeader(header) {
249
+ // const connection = this.findConnection(header);
250
+ // if (connection) {
251
+ // // connection.children.push(header);
252
+ // this.orphanReconnect();
253
+ // } else {
254
+ // this.orphanBlocks.push(header);
255
+ // }
256
+ // }
180
257
 
181
258
  /** @private
182
259
  * validates a dashcore.BlockHeader object
@@ -186,8 +263,9 @@ const SpvChain = class {
186
263
  * @return {boolean}
187
264
  */
188
265
  isValid(header, previousHeaders) {
189
- return !!(Consensus.isValidBlockHeader(header, previousHeaders, this.network)
190
- && !this.isDuplicate(header.hash));
266
+ const validBlockHeader = Consensus.isValidBlockHeader(header, previousHeaders, this.network);
267
+ const duplicate = this.isDuplicate(header.hash);
268
+ return !!validBlockHeader && !duplicate;
191
269
  }
192
270
 
193
271
  /* eslint-disable no-param-reassign */
@@ -203,24 +281,30 @@ const SpvChain = class {
203
281
  if (utils.getCorrectedHash(header.prevHash) !== previousHeader.hash) {
204
282
  return false;
205
283
  }
206
- if (!header.children) {
207
- header.children = [];
208
- }
209
- if (!previousHeader.children) {
210
- previousHeader.children = [];
211
- }
212
- previousHeader.children.push(header);
284
+ // if (!header.children) {
285
+ // header.children = [];
286
+ // }
287
+ // if (!previousHeader.children) {
288
+ // previousHeader.children = [];
289
+ // }
290
+ // previousHeader.children.push(header);
213
291
  return true;
214
292
  }
215
293
  /* eslint-enable no-param-reassign */
216
294
 
217
295
  /**
218
- * gets the longest chain
219
- *
220
- * @return {Object[]}
296
+ * Returns the longest chain of headers
297
+ * @param options
298
+ * @returns {*}
221
299
  */
222
- getLongestChain() {
223
- return this.allBranches.sort((b1, b2) => b1 < b2)[0];
300
+ getLongestChain(options = { withPruned: false }) {
301
+ let longestChain = this.allBranches.sort((b1, b2) => b1 < b2)[0];
302
+
303
+ if (options.withPruned) {
304
+ longestChain = this.prunedHeaders.concat(longestChain);
305
+ }
306
+
307
+ return longestChain;
224
308
  }
225
309
 
226
310
  /**
@@ -229,7 +313,8 @@ const SpvChain = class {
229
313
  * @return {string} hash
230
314
  */
231
315
  getTipHash() {
232
- return this.getLongestChain().slice(-1)[0].hash;
316
+ const tip = this.getTipHeader();
317
+ return tip && tip.hash;
233
318
  }
234
319
 
235
320
  /**
@@ -248,14 +333,25 @@ const SpvChain = class {
248
333
  * @return {Object} header
249
334
  */
250
335
  getHeader(hash) {
251
- return this.store.get(hash)
252
- .then((blockInDB) => {
253
- if (blockInDB) {
254
- return blockInDB;
255
- }
336
+ // TODO: perform lookup in pruned headers?
337
+ return this.getLongestChain().filter((h) => h.hash === hash)[0];
338
+ }
256
339
 
257
- return this.getLongestChain().filter((h) => h.hash === hash)[0];
258
- });
340
+ /**
341
+ * Gets specified amount of headers in the confirmed chain
342
+ * @param n
343
+ * @return Object[]
344
+ */
345
+ getLastHeaders(n) {
346
+ const longestChain = this.getLongestChain();
347
+ let headers = longestChain.slice(-n);
348
+
349
+ if (headers.length < n) {
350
+ const remaining = n - headers.length;
351
+ headers = [...this.prunedHeaders.slice(-remaining), ...headers];
352
+ }
353
+
354
+ return headers;
259
355
  }
260
356
 
261
357
  /**
@@ -266,18 +362,18 @@ const SpvChain = class {
266
362
  * @param {Object[]|string[]|buffer[]} header
267
363
  * @return {boolean}
268
364
  */
269
- addHeader(header) {
270
- const headerNormalised = utils.normalizeHeader(header);
271
-
272
- if (this.isValid(headerNormalised, this.getLongestChain())) {
273
- headerNormalised.children = [];
274
- this.processValidHeader(headerNormalised);
275
- this.setAllBranches();
276
- this.checkPruneBlocks();
277
- return true;
278
- }
279
- return false;
280
- }
365
+ // addHeader(header) {
366
+ // const headerNormalised = utils.normalizeHeader(header);
367
+ //
368
+ // if (this.isValid(headerNormalised, this.getLongestChain())) {
369
+ // // headerNormalised.children = [];
370
+ // this.processValidHeader(headerNormalised);
371
+ // this.setAllBranches();
372
+ // this.checkPruneBlocks();
373
+ // return true;
374
+ // }
375
+ // return false;
376
+ // }
281
377
 
282
378
  /**
283
379
  * adds an array of valid headers to the longest spv chain.
@@ -285,45 +381,76 @@ const SpvChain = class {
285
381
  * added to an orphan array for possible later reconnection
286
382
  *
287
383
  * @param {Object[]|string[]|buffer[]} headers
288
- * @return {boolean}
384
+ * @param {number} batchHeadHeight - height of the first header in the array
385
+ * @return {BlockHeader[]}
289
386
  */
290
- addHeaders(headers) {
291
- if (headers.length === 1) {
292
- if (!this.addHeader(headers[0])) {
293
- throw new Error('Some headers are invalid');
294
- } else {
295
- return true;
296
- }
297
- }
387
+ addHeaders(headers, batchHeadHeight = 0) {
388
+ let headHeight = batchHeadHeight;
298
389
  const normalizedHeaders = headers.map((h) => utils.normalizeHeader(h));
299
- const isOrphan = !SpvChain.isParentChild(normalizedHeaders[0], this.getTipHeader());
390
+
391
+ const tip = this.getTipHeader();
392
+ // Handle 1 block intersection of batches
393
+ if (tip && tip.hash === normalizedHeaders[0].hash) {
394
+ normalizedHeaders.splice(0, 1);
395
+ // Patch head height value after splice
396
+ headHeight += 1;
397
+ }
398
+
399
+ if (normalizedHeaders.length === 0) {
400
+ // The batch already in the chain, do nothing
401
+ return [];
402
+ }
403
+
404
+ const firstHeader = normalizedHeaders[0];
405
+ const connectsToTip = tip && SpvChain.isParentChild(firstHeader, tip);
406
+
407
+ //
408
+ // Reorg detection
409
+ // Get prev header hash
410
+ const prevHash = utils.getCorrectedHash(firstHeader.prevHash);
411
+ const prevHeaderHeight = this.heightByHash.get(prevHash);
412
+
413
+ // Test on initial wallet load
414
+ if (prevHeaderHeight && prevHeaderHeight > 0 && prevHeaderHeight !== headHeight - 1) {
415
+ console.log('SPVCHAIN: Reorg detected.');
416
+ console.log(`------->: Batch head ${firstHeader.hash} at height ${headHeight} has parent at height ${prevHeaderHeight}`);
417
+ }
418
+
419
+ const isOrphan = tip ? !connectsToTip
420
+ : headHeight !== this.startBlockHeight;
300
421
 
301
422
  const allValid = normalizedHeaders.reduce(
302
423
  (acc, header, index, array) => {
303
424
  const previousHeaders = normalizedHeaders.slice(0, index);
304
425
  if (index !== 0) {
305
- if (!SpvChain.isParentChild(header, array[index - 1])
306
- || !this.isValid(header, previousHeaders)) {
307
- throw new Error('Some headers are invalid');
426
+ if (!SpvChain.isParentChild(header, array[index - 1])) {
427
+ throw new SPVError(`SPV: Header ${header.hash} is not a child of ${array[index - 1].hash}`);
428
+ }
429
+
430
+ if (!this.isValid(header, previousHeaders)) {
431
+ throw new SPVError(`SPV: Header ${header.hash} is invalid`);
308
432
  }
309
433
  return acc && true;
310
434
  }
311
435
  if (isOrphan) {
312
436
  if (!this.isValid(header, previousHeaders)) {
313
- throw new Error('Some headers are invalid');
437
+ throw new SPVError('Some headers are invalid');
314
438
  }
315
439
  return acc && true;
316
440
  }
317
441
  if (!this.isValid(header, this.getLongestChain())) {
318
- throw new Error('Some headers are invalid');
442
+ throw new SPVError('Some headers are invalid');
319
443
  }
320
444
  return acc && true;
321
445
  }, true,
322
446
  );
323
447
  if (!allValid) {
324
- throw new Error('Some headers are invalid');
448
+ throw new SPVError('Some headers are invalid');
325
449
  }
326
450
  if (isOrphan) {
451
+ normalizedHeaders.forEach((header) => {
452
+ this.orphansHashes.add(header.hash);
453
+ });
327
454
  this.orphanChunks.push(normalizedHeaders);
328
455
  } else {
329
456
  this.appendHeadersToLongestChain(normalizedHeaders);
@@ -332,7 +459,7 @@ const SpvChain = class {
332
459
  this.orphanChunksReconnect();
333
460
  }
334
461
  this.checkPruneBlocks();
335
- return true;
462
+ return normalizedHeaders;
336
463
  }
337
464
  };
338
465
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dashevo/dash-spv",
3
- "version": "0.24.0-dev.6",
3
+ "version": "0.25.0-dev.1",
4
4
  "description": "Repository containing SPV functions used by @dashevo",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -14,11 +14,12 @@
14
14
  "dependencies": {
15
15
  "@dashevo/dark-gravity-wave": "^1.1.1",
16
16
  "@dashevo/dash-util": "^2.0.3",
17
- "@dashevo/dashcore-lib": "~0.19.44",
17
+ "@dashevo/dashcore-lib": "~0.20.3",
18
18
  "levelup": "^4.4.0",
19
19
  "memdown": "^5.1.0"
20
20
  },
21
21
  "devDependencies": {
22
+ "chai": "^4.3.4",
22
23
  "eslint": "^7.32.0",
23
24
  "eslint-config-airbnb-base": "^14.2.1",
24
25
  "eslint-plugin-import": "^2.24.2",
package/test/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  const { MerkleBlock, Transaction } = require('@dashevo/dashcore-lib');
2
+ const should = require('should');
3
+
2
4
  const Blockchain = require('../lib/spvchain');
3
5
  const utils = require('../lib/utils');
4
6
  const merkleProofs = require('../lib/merkleproofs');
@@ -13,11 +15,9 @@ let chain = null;
13
15
  let merkleBlock = null;
14
16
  let merkleBlock2 = null;
15
17
 
16
- require('should');
17
-
18
18
  describe('SPV-DASH (forks & re-orgs) deserialized headers', () => {
19
19
  before(() => {
20
- chain = new Blockchain('devnet');
20
+ chain = new Blockchain('testnet');
21
21
  });
22
22
 
23
23
  it('should get 26 testnet headers', () => {
@@ -34,39 +34,40 @@ describe('SPV-DASH (forks & re-orgs) deserialized headers', () => {
34
34
  });
35
35
 
36
36
  it('should still contain a branch of 1 when first header is added', () => {
37
- chain.addHeader(headers[0]);
37
+ chain.addHeaders([headers[0]]);
38
38
  chain.getAllBranches().length.should.equal(1);
39
39
  chain.getLongestChain().length.should.equal(2);
40
40
  });
41
41
 
42
42
  it('should discard adding of duplicate block', () => {
43
- chain.addHeader(headers[0]);
44
- chain.getOrphans().length.should.equal(0);
43
+ chain.addHeaders([headers[0]]);
44
+ chain.getOrphanChunks().length.should.equal(0);
45
45
  chain.getLongestChain().length.should.equal(2);
46
46
  });
47
47
 
48
48
  it('should create 1 orphan', () => {
49
- chain.addHeader(headers[2]);
50
- chain.getOrphans().length.should.equal(1);
49
+ chain.addHeaders([headers[2]]);
50
+ chain.getOrphanChunks().length.should.equal(1);
51
51
  chain.getLongestChain().length.should.equal(2);
52
52
  });
53
53
 
54
54
  it('should connect the orphan by adding its parent', () => {
55
- chain.addHeader(headers[1]);
56
- chain.getOrphans().length.should.equal(0);
55
+ chain.addHeaders([headers[1]]);
56
+ chain.getOrphanChunks().length.should.equal(0);
57
57
  chain.getAllBranches().length.should.equal(1);
58
58
  chain.getLongestChain().length.should.equal(4);
59
59
  });
60
60
 
61
61
  it('should add remaining test headers', () => {
62
62
  chain.addHeaders(headers.slice(3, 24));
63
- chain.getOrphans().length.should.equal(0);
63
+ chain.getOrphanChunks().length.should.equal(0);
64
64
  chain.getAllBranches().length.should.equal(1);
65
65
  chain.getLongestChain().length.should.equal(25);
66
66
  });
67
67
 
68
- it('not add an invalid header', () => {
69
- chain.addHeader(headers[25]);
68
+ // TODO: restore when conesnsus rules are enabled
69
+ it.skip('not add an invalid header', () => {
70
+ should(() => chain.addHeaders([headers[25]])).throw();
70
71
  chain.getLongestChain().length.should.equal(25);
71
72
  });
72
73
 
@@ -75,7 +76,7 @@ describe('SPV-DASH (forks & re-orgs) deserialized headers', () => {
75
76
  chain.addHeaders([headers[25], headers[10]]);
76
77
  done(new Error('SPV chain failed to throw an error on invalid block'));
77
78
  } catch (e) {
78
- e.message.should.equal('Some headers are invalid');
79
+ e.message.should.equal('SPV: Header 00000ce430de949c85a145b02e33ebbaed3772dc8f3d668f66edc6852c24d002 is not a child of e59b97d3d11b4d4563b557e32c62afbdb6e947e9cbb4915c94073f08f7936d5f');
79
80
  done();
80
81
  }
81
82
  });
@@ -100,26 +101,26 @@ describe('SPV-DASH (forks & re-orgs) serialized raw headers for mainnet', () =>
100
101
  });
101
102
 
102
103
  it('should still contain a branch of 1 when first header is added', () => {
103
- chain.addHeader(mainnet[1]);
104
+ chain.addHeaders([mainnet[1]]);
104
105
  chain.getAllBranches().length.should.equal(1);
105
106
  chain.getLongestChain().length.should.equal(2);
106
107
  });
107
108
 
108
109
  it('should discard addding of duplicate block', () => {
109
- chain.addHeader(mainnet[1]);
110
- chain.getOrphans().length.should.equal(0);
110
+ chain.addHeaders([mainnet[1]]);
111
+ chain.getOrphanChunks().length.should.equal(0);
111
112
  chain.getLongestChain().length.should.equal(2);
112
113
  });
113
114
 
114
115
  it('should create 1 orphan', () => {
115
- chain.addHeader(mainnet[3]);
116
- chain.getOrphans().length.should.equal(1);
116
+ chain.addHeaders([mainnet[3]]);
117
+ chain.getOrphanChunks().length.should.equal(1);
117
118
  chain.getLongestChain().length.should.equal(2);
118
119
  });
119
120
 
120
121
  it('should connect the orphan by adding its parent', () => {
121
- chain.addHeader(mainnet[2]);
122
- chain.getOrphans().length.should.equal(0);
122
+ chain.addHeaders([mainnet[2]]);
123
+ chain.getOrphanChunks().length.should.equal(0);
123
124
  chain.getAllBranches().length.should.equal(1);
124
125
  chain.getLongestChain().length.should.equal(4);
125
126
  });
@@ -132,14 +133,14 @@ describe('SPV-DASH (addHeaders) add many headers for testnet', () => {
132
133
 
133
134
  it('should add the 1st 250 testnet headers', () => {
134
135
  chain.addHeaders(testnet.slice(1, 250));
135
- chain.getOrphans().length.should.equal(0);
136
+ chain.getOrphanChunks().length.should.equal(0);
136
137
  chain.getAllBranches().length.should.equal(1);
137
138
  chain.getLongestChain().length.should.equal(250);
138
139
  });
139
140
 
140
141
  it('should add the next 250 (250 - 500) testnet headers', () => {
141
142
  chain.addHeaders(testnet.slice(250, 500));
142
- chain.getOrphans().length.should.equal(0);
143
+ chain.getOrphanChunks().length.should.equal(0);
143
144
  chain.getAllBranches().length.should.equal(1);
144
145
  chain.getLongestChain().length.should.equal(500);
145
146
  });
@@ -147,14 +148,14 @@ describe('SPV-DASH (addHeaders) add many headers for testnet', () => {
147
148
  it('should add the 1st 250 testnet2 headers', () => {
148
149
  chain = new Blockchain('testnet', 10000, utils.normalizeHeader(testnet2[0]));
149
150
  chain.addHeaders(testnet2.slice(1, 250));
150
- chain.getOrphans().length.should.equal(0);
151
+ chain.getOrphanChunks().length.should.equal(0);
151
152
  chain.getAllBranches().length.should.equal(1);
152
153
  chain.getLongestChain().length.should.equal(250);
153
154
  });
154
155
 
155
156
  it('should add the next 250 (250 - 500) testnet2 headers', () => {
156
157
  chain.addHeaders(testnet2.slice(250, 500));
157
- chain.getOrphans().length.should.equal(0);
158
+ chain.getOrphanChunks().length.should.equal(0);
158
159
  chain.getAllBranches().length.should.equal(1);
159
160
  chain.getLongestChain().length.should.equal(500);
160
161
  });
@@ -162,26 +163,26 @@ describe('SPV-DASH (addHeaders) add many headers for testnet', () => {
162
163
  it('should add the 1st 250 testnet3 headers', () => {
163
164
  chain = new Blockchain('testnet', 10000, utils.normalizeHeader(testnet3[0]));
164
165
  chain.addHeaders(testnet3.slice(1, 250));
165
- chain.getOrphans().length.should.equal(0);
166
+ chain.getOrphanChunks().length.should.equal(0);
166
167
  chain.getAllBranches().length.should.equal(1);
167
168
  chain.getLongestChain().length.should.equal(250);
168
169
  });
169
170
 
170
171
  it('should add the next 250 (250 - 500) testnet3 headers', () => {
171
172
  chain.addHeaders(testnet3.slice(250, 500));
172
- chain.getOrphans().length.should.equal(0);
173
+ chain.getOrphanChunks().length.should.equal(0);
173
174
  chain.getAllBranches().length.should.equal(1);
174
175
  chain.getLongestChain().length.should.equal(500);
175
176
  });
176
177
 
177
178
  it('should not add an invalid header', () => {
178
- chain.addHeader(testnet[499]);
179
+ chain.addHeaders([testnet[499]]);
179
180
  chain.getLongestChain().length.should.equal(500);
180
181
  });
181
182
 
182
183
  it('should orphan and not add invalid but consistent headers', () => {
183
184
  chain.addHeaders([badRawHeaders[0], badRawHeaders[1]]);
184
- chain.getOrphanChunks().length.should.equal(1);
185
+ chain.getOrphanChunks().length.should.equal(2);
185
186
  chain.getLongestChain().length.should.equal(500);
186
187
  });
187
188
 
@@ -203,7 +204,7 @@ describe('SPV-DASH (addHeaders) add testnet headers out of order', () => {
203
204
 
204
205
  it('should add the 1st 100 testnet headers', () => {
205
206
  chain.addHeaders(testnet.slice(1, 100));
206
- chain.getOrphans().length.should.equal(0);
207
+ chain.getOrphanChunks().length.should.equal(0);
207
208
  chain.getAllBranches().length.should.equal(1);
208
209
  chain.getOrphanChunks().length.should.equal(0);
209
210
  chain.getLongestChain().length.should.equal(100);
@@ -211,7 +212,6 @@ describe('SPV-DASH (addHeaders) add testnet headers out of order', () => {
211
212
 
212
213
  it('should orphan testnet headers 200 - 300', () => {
213
214
  chain.addHeaders(testnet.slice(200, 300));
214
- chain.getOrphans().length.should.equal(0);
215
215
  chain.getAllBranches().length.should.equal(1);
216
216
  chain.getOrphanChunks().length.should.equal(1);
217
217
  chain.getLongestChain().length.should.equal(100);
@@ -219,15 +219,13 @@ describe('SPV-DASH (addHeaders) add testnet headers out of order', () => {
219
219
 
220
220
  it('should orphan testnet headers 400 - 500', () => {
221
221
  chain.addHeaders(testnet.slice(400, 500));
222
- chain.getOrphans().length.should.equal(0);
223
- chain.getAllBranches().length.should.equal(1);
224
222
  chain.getOrphanChunks().length.should.equal(2);
223
+ chain.getAllBranches().length.should.equal(1);
225
224
  chain.getLongestChain().length.should.equal(100);
226
225
  });
227
226
 
228
227
  it('should reconnect orphaned chunks (testnet headers 1 - 100 and 200 - 300)', () => {
229
228
  chain.addHeaders(testnet.slice(100, 200));
230
- chain.getOrphans().length.should.equal(0);
231
229
  chain.getAllBranches().length.should.equal(1);
232
230
  chain.getOrphanChunks().length.should.equal(1);
233
231
  chain.getLongestChain().length.should.equal(300);
@@ -235,7 +233,7 @@ describe('SPV-DASH (addHeaders) add testnet headers out of order', () => {
235
233
 
236
234
  it('should reconnect orphaned chunks (testnet headers 1 - 300 and 400 - 500)', () => {
237
235
  chain.addHeaders(testnet.slice(300, 400));
238
- chain.getOrphans().length.should.equal(0);
236
+ chain.getOrphanChunks().length.should.equal(0);
239
237
  chain.getAllBranches().length.should.equal(1);
240
238
  chain.getOrphanChunks().length.should.equal(0);
241
239
  chain.getLongestChain().length.should.equal(500);
@@ -249,27 +247,27 @@ describe('SPV-DASH (addHeaders) add many headers for mainnet', () => {
249
247
 
250
248
  it('should add the 1st 500 mainnet headers', () => {
251
249
  chain.addHeaders(mainnet.slice(1, 500));
252
- chain.getOrphans().length.should.equal(0);
250
+ chain.getOrphanChunks().length.should.equal(0);
253
251
  chain.getAllBranches().length.should.equal(1);
254
252
  chain.getLongestChain().length.should.equal(500);
255
253
  });
256
254
 
257
255
  it('should add the next 500 (500 - 1000) mainnet headers', () => {
258
256
  chain.addHeaders(mainnet.slice(500, 1000));
259
- chain.getOrphans().length.should.equal(0);
257
+ chain.getOrphanChunks().length.should.equal(0);
260
258
  chain.getAllBranches().length.should.equal(1);
261
259
  chain.getLongestChain().length.should.equal(1000);
262
260
  });
263
261
 
264
262
  it('should add the next 500 (1000 - 1500) mainnet headers', () => {
265
263
  chain.addHeaders(mainnet.slice(1000, 1500));
266
- chain.getOrphans().length.should.equal(0);
264
+ chain.getOrphanChunks().length.should.equal(0);
267
265
  chain.getAllBranches().length.should.equal(1);
268
266
  chain.getLongestChain().length.should.equal(1500);
269
267
  });
270
268
 
271
269
  it('should not add an invalid header', () => {
272
- chain.addHeader(mainnet[499]);
270
+ should(() => chain.addHeaders([mainnet[499]])).throw();
273
271
  chain.getLongestChain().length.should.equal(1500);
274
272
  });
275
273
 
@@ -297,7 +295,7 @@ describe('SPV-DASH (addHeaders) add mainnet headers out of order', () => {
297
295
 
298
296
  it('should add the 1st 100 mainnet headers', () => {
299
297
  chain.addHeaders(mainnet.slice(1, 100));
300
- chain.getOrphans().length.should.equal(0);
298
+ chain.getOrphanChunks().length.should.equal(0);
301
299
  chain.getAllBranches().length.should.equal(1);
302
300
  chain.getOrphanChunks().length.should.equal(0);
303
301
  chain.getLongestChain().length.should.equal(100);
@@ -305,7 +303,6 @@ describe('SPV-DASH (addHeaders) add mainnet headers out of order', () => {
305
303
 
306
304
  it('should orphan mainnet headers 200 - 300', () => {
307
305
  chain.addHeaders(mainnet.slice(200, 300));
308
- chain.getOrphans().length.should.equal(0);
309
306
  chain.getAllBranches().length.should.equal(1);
310
307
  chain.getOrphanChunks().length.should.equal(1);
311
308
  chain.getLongestChain().length.should.equal(100);
@@ -313,7 +310,6 @@ describe('SPV-DASH (addHeaders) add mainnet headers out of order', () => {
313
310
 
314
311
  it('should orphan mainnet headers 400 - 500', () => {
315
312
  chain.addHeaders(mainnet.slice(400, 500));
316
- chain.getOrphans().length.should.equal(0);
317
313
  chain.getAllBranches().length.should.equal(1);
318
314
  chain.getOrphanChunks().length.should.equal(2);
319
315
  chain.getLongestChain().length.should.equal(100);
@@ -321,7 +317,6 @@ describe('SPV-DASH (addHeaders) add mainnet headers out of order', () => {
321
317
 
322
318
  it('should reconnect orphaned chunks (mainnet headers 1 - 100 and 200 - 300)', () => {
323
319
  chain.addHeaders(mainnet.slice(100, 200));
324
- chain.getOrphans().length.should.equal(0);
325
320
  chain.getAllBranches().length.should.equal(1);
326
321
  chain.getOrphanChunks().length.should.equal(1);
327
322
  chain.getLongestChain().length.should.equal(300);
@@ -329,7 +324,6 @@ describe('SPV-DASH (addHeaders) add mainnet headers out of order', () => {
329
324
 
330
325
  it('should reconnect orphaned chunks (mainnet headers 1 - 300 and 400 - 500)', () => {
331
326
  chain.addHeaders(mainnet.slice(300, 400));
332
- chain.getOrphans().length.should.equal(0);
333
327
  chain.getAllBranches().length.should.equal(1);
334
328
  chain.getOrphanChunks().length.should.equal(0);
335
329
  chain.getLongestChain().length.should.equal(500);
@@ -337,7 +331,7 @@ describe('SPV-DASH (addHeaders) add mainnet headers out of order', () => {
337
331
  });
338
332
 
339
333
  let genesisHash = null;
340
- describe('Blockstore', () => {
334
+ describe.skip('Blockstore', () => {
341
335
  before(() => {
342
336
  chain = new Blockchain('testnet', 10);
343
337
  genesisHash = chain.getTipHash();
@@ -0,0 +1,33 @@
1
+ const { expect } = require('chai');
2
+ const { SpvChain } = require('../index');
3
+
4
+ const { testnet } = require('./data/rawHeaders');
5
+
6
+ describe('SPVChain', () => {
7
+ let spvChain;
8
+
9
+ beforeEach(() => {
10
+ spvChain = new SpvChain('testnet', 100);
11
+ });
12
+
13
+ describe('#addHeaders', () => {
14
+ it('should assemble headers chain if headers arriving out of order', () => {
15
+ expect(true).to.equal(true);
16
+ spvChain.reset(10000);
17
+
18
+ spvChain.addHeaders(testnet.slice(400), 10400);
19
+ expect(spvChain.getOrphanChunks()).to.have.length(1);
20
+ spvChain.addHeaders(testnet.slice(200, 300), 10200);
21
+ expect(spvChain.getOrphanChunks()).to.have.length(2);
22
+ spvChain.addHeaders(testnet.slice(300, 400), 10300);
23
+ expect(spvChain.getOrphanChunks()).to.have.length(3);
24
+ spvChain.addHeaders(testnet.slice(100, 200), 10100);
25
+ expect(spvChain.getOrphanChunks()).to.have.length(4);
26
+ spvChain.addHeaders(testnet.slice(0, 100), 10000);
27
+
28
+ const longestChain = spvChain.getLongestChain({ withPruned: true });
29
+ expect(longestChain).to.have.length(testnet.length);
30
+ expect(spvChain.getOrphanChunks()).to.have.length(0);
31
+ });
32
+ });
33
+ });