@bsv/sdk 2.0.11 → 2.0.12

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.
Files changed (43) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/overlay-tools/HostReputationTracker.js +21 -13
  3. package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
  4. package/dist/cjs/src/primitives/PrivateKey.js +3 -3
  5. package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
  6. package/dist/cjs/src/script/Spend.js +17 -9
  7. package/dist/cjs/src/script/Spend.js.map +1 -1
  8. package/dist/cjs/src/storage/StorageDownloader.js +6 -6
  9. package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
  10. package/dist/cjs/src/storage/StorageUtils.js +1 -1
  11. package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
  12. package/dist/cjs/src/transaction/MerklePath.js +36 -27
  13. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  14. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  15. package/dist/esm/src/overlay-tools/HostReputationTracker.js +21 -13
  16. package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
  17. package/dist/esm/src/primitives/PrivateKey.js +3 -3
  18. package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
  19. package/dist/esm/src/script/Spend.js +17 -9
  20. package/dist/esm/src/script/Spend.js.map +1 -1
  21. package/dist/esm/src/storage/StorageDownloader.js +6 -6
  22. package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
  23. package/dist/esm/src/storage/StorageUtils.js +1 -1
  24. package/dist/esm/src/storage/StorageUtils.js.map +1 -1
  25. package/dist/esm/src/transaction/MerklePath.js +36 -27
  26. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  27. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  28. package/dist/types/src/overlay-tools/HostReputationTracker.d.ts.map +1 -1
  29. package/dist/types/src/script/Spend.d.ts.map +1 -1
  30. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  31. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  32. package/dist/umd/bundle.js +3 -3
  33. package/dist/umd/bundle.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/auth/utils/__tests/validateCertificates.test.ts +12 -9
  36. package/src/kvstore/__tests/LocalKVStore.test.ts +4 -6
  37. package/src/overlay-tools/HostReputationTracker.ts +17 -14
  38. package/src/primitives/PrivateKey.ts +3 -3
  39. package/src/script/Spend.ts +19 -11
  40. package/src/storage/StorageDownloader.ts +6 -6
  41. package/src/storage/StorageUtils.ts +1 -1
  42. package/src/transaction/MerklePath.ts +41 -36
  43. package/src/transaction/__tests/MerklePath.test.ts +152 -21
@@ -112,6 +112,24 @@ const BRC74JSONTrimmed = {
112
112
  }
113
113
  BRC74JSONTrimmed.path[1] = []
114
114
 
115
+ const BLOCK_125632 = {
116
+ height: 125632,
117
+ merkleroot: '205b2e27c58601fc1a8de04c83b6b0c46f89c16b2161c93441b7e9269cf6bc4a',
118
+ tx: [
119
+ '17cba98da71fe75862aac894392f2ff604356db386767fec364877a5a9ff200c',
120
+ '14ce64bd223ec9bb42662b74fdcf94f96a209a1aee72b7ba7639db503150ec2e',
121
+ '90a2de85351cfadd2326b9b0098e9c453af09b2980835f57a1429bbb44beb872',
122
+ 'a31f2ddfea7ddd4581dca3007ee99e58ea6baa97a8ac3b32bb4610baac9f7206',
123
+ 'c36eeed6fbc0259d30804f59f804dfcda35a54461157d6ac9c094f0ea378f35c',
124
+ '17752483868c52a98407a0e226d73b42e214e0fad548541619d858e1fd4a9549',
125
+ '3b8c4460412cfc55be0d50308ba704a859bd6f83bfed01b0828c9b067cd69246',
126
+ 'a3f1b9d4b3ef3b061af352fdc2d02048417030fef9282c36da689cd899437cdb',
127
+ '66e2b022da877621ef197e02c3ef7d3f820d33a86ead2e72bf966432ea6776f1',
128
+ 'e988b5d7a2cec8e0759ade2e151737d1cdfdde68accff42938583ad12eb98b99',
129
+ '5e7a8a8ec3f912ac1c4e90279c04263f170ed055c0411c8d490b846f01e6a99e'
130
+ ]
131
+ }
132
+
115
133
  const BRC74Root =
116
134
  '57aab6e6fb1b697174ffb64e062c4728f2ffd33ddcfa02a43b64d8cd29b483b4'
117
135
  const BRC74TXID1 =
@@ -200,10 +218,9 @@ describe('MerklePath', () => {
200
218
  it('Serializes and deserializes a combined trimmed path', () => {
201
219
  const [pathA, pathB] = buildSplitPaths()
202
220
  pathA.combine(pathB)
203
- let deserialized: MerklePath
204
- expect(() => { deserialized = MerklePath.fromHex(pathA.toHex()) }).not.toThrow()
205
- expect(deserialized!.computeRoot(BRC74TXID2)).toEqual(BRC74Root)
206
- expect(deserialized!.computeRoot(BRC74TXID3)).toEqual(BRC74Root)
221
+ const deserialized = MerklePath.fromHex(pathA.toHex())
222
+ expect(deserialized.computeRoot(BRC74TXID2)).toEqual(BRC74Root)
223
+ expect(deserialized.computeRoot(BRC74TXID3)).toEqual(BRC74Root)
207
224
  })
208
225
  it('Constructs a compound path from all txids at level 0 only', () => {
209
226
  // A single-level compound path: all txids for a block given at level 0, no higher levels.
@@ -213,24 +230,20 @@ describe('MerklePath', () => {
213
230
  const tx2 = 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
214
231
  const tx3 = 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'
215
232
  const root4 = merkleHash(merkleHash(tx3 + tx2) + merkleHash(tx1 + tx0))
216
- let mp: MerklePath
217
- expect(() => {
218
- mp = new MerklePath(100, [[
219
- { offset: 0, txid: true, hash: tx0 },
220
- { offset: 1, txid: true, hash: tx1 },
221
- { offset: 2, txid: true, hash: tx2 },
222
- { offset: 3, txid: true, hash: tx3 }
223
- ]])
224
- }).not.toThrow()
225
- expect(mp!.computeRoot(tx0)).toEqual(root4)
226
- expect(mp!.computeRoot(tx1)).toEqual(root4)
227
- expect(mp!.computeRoot(tx2)).toEqual(root4)
228
- expect(mp!.computeRoot(tx3)).toEqual(root4)
233
+ const mp = new MerklePath(100, [[
234
+ { offset: 0, txid: true, hash: tx0 },
235
+ { offset: 1, txid: true, hash: tx1 },
236
+ { offset: 2, txid: true, hash: tx2 },
237
+ { offset: 3, txid: true, hash: tx3 }
238
+ ]])
239
+ expect(mp.computeRoot(tx0)).toEqual(root4)
240
+ expect(mp.computeRoot(tx1)).toEqual(root4)
241
+ expect(mp.computeRoot(tx2)).toEqual(root4)
242
+ expect(mp.computeRoot(tx3)).toEqual(root4)
229
243
  // Serializing and deserializing a single-level compound path should also work
230
- let deserialized: MerklePath
231
- expect(() => { deserialized = MerklePath.fromHex(mp!.toHex()) }).not.toThrow()
232
- expect(deserialized!.computeRoot(tx0)).toEqual(root4)
233
- expect(deserialized!.computeRoot(tx3)).toEqual(root4)
244
+ const deserialized = MerklePath.fromHex(mp.toHex())
245
+ expect(deserialized.computeRoot(tx0)).toEqual(root4)
246
+ expect(deserialized.computeRoot(tx3)).toEqual(root4)
234
247
  })
235
248
  it('Rejects invalid bumps', () => {
236
249
  for (const invalid of invalidBumps) {
@@ -284,4 +297,122 @@ describe('MerklePath', () => {
284
297
  )
285
298
  expect(isValid).toBe(false)
286
299
  })
300
+ it('constructs a compound MerklePath from all txids in a block with odd tree levels', () => {
301
+ const { height, merkleroot, tx } = BLOCK_125632
302
+ const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
303
+ if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
304
+ const mp = new MerklePath(height, [leafs])
305
+ expect(mp.computeRoot()).toBe(merkleroot)
306
+ })
307
+ it('compound path for 3 txids trims, round-trips through hex, and splits into per-txid proofs', () => {
308
+ const { height, merkleroot, tx } = BLOCK_125632
309
+
310
+ // Precompute the full Merkle tree for block 125632.
311
+ // merkleHash(right + left) matches the SDK's internal hash convention.
312
+ const L1 = [
313
+ merkleHash(tx[1] + tx[0]),
314
+ merkleHash(tx[3] + tx[2]),
315
+ merkleHash(tx[5] + tx[4]),
316
+ merkleHash(tx[7] + tx[6]),
317
+ merkleHash(tx[9] + tx[8]),
318
+ merkleHash(tx[10] + tx[10]) // tx[10] duplicated — odd count at level 0
319
+ ]
320
+ const L2 = [
321
+ merkleHash(L1[1] + L1[0]),
322
+ merkleHash(L1[3] + L1[2]),
323
+ merkleHash(L1[5] + L1[4])
324
+ ]
325
+ const L3 = [
326
+ merkleHash(L2[1] + L2[0]),
327
+ merkleHash(L2[2] + L2[2]) // L2 count = 3 (odd) — last node duplicated
328
+ ]
329
+ expect(merkleHash(L3[1] + L3[0])).toBe(merkleroot)
330
+
331
+ // Build minimal per-txid MerklePaths for tx[2], tx[5], and tx[8].
332
+ // tx[8] exercises the odd-level duplication at level 2 ({offset:3, duplicate:true}).
333
+ const mpTx2 = new MerklePath(height, [
334
+ [{ offset: 2, txid: true, hash: tx[2] }, { offset: 3, hash: tx[3] }],
335
+ [{ offset: 0, hash: L1[0] }],
336
+ [{ offset: 1, hash: L2[1] }],
337
+ [{ offset: 1, hash: L3[1] }]
338
+ ])
339
+ const mpTx5 = new MerklePath(height, [
340
+ [{ offset: 4, hash: tx[4] }, { offset: 5, txid: true, hash: tx[5] }],
341
+ [{ offset: 3, hash: L1[3] }],
342
+ [{ offset: 0, hash: L2[0] }],
343
+ [{ offset: 1, hash: L3[1] }]
344
+ ])
345
+ const mpTx8 = new MerklePath(height, [
346
+ [{ offset: 8, txid: true, hash: tx[8] }, { offset: 9, hash: tx[9] }],
347
+ [{ offset: 5, hash: L1[5] }],
348
+ [{ offset: 3, duplicate: true }], // tx[8] is last odd node at level 2
349
+ [{ offset: 0, hash: L3[0] }]
350
+ ])
351
+ expect(mpTx2.computeRoot(tx[2])).toBe(merkleroot)
352
+ expect(mpTx5.computeRoot(tx[5])).toBe(merkleroot)
353
+ expect(mpTx8.computeRoot(tx[8])).toBe(merkleroot)
354
+
355
+ // Combine into one compound path (combine() trims automatically)
356
+ const compound = new MerklePath(height, mpTx2.path.map(l => [...l]))
357
+ compound.combine(mpTx5)
358
+ compound.combine(mpTx8)
359
+ expect(compound.computeRoot(tx[2])).toBe(merkleroot)
360
+ expect(compound.computeRoot(tx[5])).toBe(merkleroot)
361
+ expect(compound.computeRoot(tx[8])).toBe(merkleroot)
362
+
363
+ // Serialize and deserialize
364
+ const deserialized = MerklePath.fromHex(compound.toHex())
365
+ expect(deserialized.computeRoot(tx[2])).toBe(merkleroot)
366
+ expect(deserialized.computeRoot(tx[5])).toBe(merkleroot)
367
+ expect(deserialized.computeRoot(tx[8])).toBe(merkleroot)
368
+
369
+ // Split the deserialized compound path into standalone per-txid proofs.
370
+ // findOrComputeLeaf reconstructs sibling hashes that were trimmed away.
371
+ const splitProof = (source: MerklePath, txOffset: number, txHash: string): MerklePath => {
372
+ const levels = source.path.map((_, h) => {
373
+ const sibOffset = (txOffset >> h) ^ 1
374
+ if (h === 0) {
375
+ const sib = source.findOrComputeLeaf(0, sibOffset)
376
+ if (sib == null) throw new Error('Missing sibling at level 0')
377
+ return [{ offset: txOffset, txid: true, hash: txHash }, sib].sort((a, b) => a.offset - b.offset)
378
+ }
379
+ const sib = source.findOrComputeLeaf(h, sibOffset)
380
+ return sib == null ? [] : [sib]
381
+ })
382
+ return new MerklePath(source.blockHeight, levels)
383
+ }
384
+
385
+ const splitTx2 = splitProof(deserialized, 2, tx[2])
386
+ const splitTx5 = splitProof(deserialized, 5, tx[5])
387
+ const splitTx8 = splitProof(deserialized, 8, tx[8])
388
+
389
+ // Each standalone proof computes the same root — no data was lost through the pipeline
390
+ expect(splitTx2.computeRoot(tx[2])).toBe(merkleroot)
391
+ expect(splitTx5.computeRoot(tx[5])).toBe(merkleroot)
392
+ expect(splitTx8.computeRoot(tx[8])).toBe(merkleroot)
393
+ })
394
+ it('findOrComputeLeaf duplicates leaf0 when leaf1 carries both a hash and duplicate=true', () => {
395
+ // Covers the leaf1.duplicate === true branch inside findOrComputeLeaf.
396
+ // That branch is reached when leaf1.hash is non-null (bypassing the null-check above it)
397
+ // but leaf1.duplicate is also true — an unusual but valid interface state.
398
+ const tx0 = 'aa'.repeat(32)
399
+ const tx1 = 'bb'.repeat(32)
400
+
401
+ // Build a minimal valid path so the constructor does not throw.
402
+ const mp = new MerklePath(1, [[
403
+ { offset: 0, txid: true, hash: tx0 },
404
+ { offset: 1, hash: tx1 }
405
+ ]])
406
+
407
+ // Mutate: give the sibling leaf at offset 1 both a hash and duplicate=true.
408
+ // findOrComputeLeaf(1, 0) will:
409
+ // - not find offset 0 in path[1] (path.length === 1, no higher levels)
410
+ // - recurse to level 0: leaf0 = tx0 (offset 0), leaf1 = {hash:tx1, duplicate:true}
411
+ // - leaf1.hash is non-null → skips the null-branch
412
+ // - leaf1.duplicate === true → line 349: workinghash = hash(leaf0 + leaf0)
413
+ mp.path[0][1] = { offset: 1, hash: tx1, duplicate: true }
414
+
415
+ const result = mp.findOrComputeLeaf(1, 0)
416
+ expect(result?.hash).toBe(merkleHash(tx0 + tx0))
417
+ })
287
418
  })