@bsv/sdk 2.0.10 → 2.0.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -281,9 +281,13 @@ export default class MerklePath {
281
281
  // special case for blocks with only one transaction
282
282
  if (this.path.length === 1 && this.path[0].length === 1) return workingHash
283
283
 
284
- for (let height = 0; height < this.path.length; height++) {
285
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
286
- const leaves = this.path[height]
284
+ // Determine effective tree height. For a compound path where all txids are at level 0
285
+ // (path.length === 1 or intermediate levels are empty/trimmed), we need to compute up
286
+ // to the height implied by the highest offset present in path[0].
287
+ const maxOffset = this.path[0].reduce((max, l) => Math.max(max, l.offset), 0)
288
+ const treeHeight = Math.max(this.path.length, 32 - Math.clz32(maxOffset))
289
+
290
+ for (let height = 0; height < treeHeight; height++) {
287
291
  const offset = (index >> height) ^ 1
288
292
  const leaf = this.findOrComputeLeaf(height, offset)
289
293
  if (typeof leaf !== 'object') {
@@ -315,9 +319,9 @@ export default class MerklePath {
315
319
  const hash = (m: string): string =>
316
320
  toHex(hash256(toArray(m, 'hex').reverse()).reverse())
317
321
 
318
- let leaf: MerklePathLeaf | undefined = this.path[height].find(
319
- (l) => l.offset === offset
320
- )
322
+ let leaf: MerklePathLeaf | undefined = height < this.path.length
323
+ ? this.path[height].find((l) => l.offset === offset)
324
+ : undefined
321
325
 
322
326
  if (leaf != null) return leaf
323
327
 
@@ -1,8 +1,12 @@
1
1
  import ChainTracker from '../ChainTracker'
2
2
  import MerklePath from '../../transaction/MerklePath'
3
+ import { hash256 } from '../../primitives/Hash'
4
+ import { toHex, toArray } from '../../primitives/utils'
3
5
  import invalidBumps from './bump.invalid.vectors'
4
6
  import validBumps from './bump.valid.vectors'
5
7
 
8
+ const merkleHash = (m: string): string => toHex(hash256(toArray(m, 'hex').reverse()).reverse())
9
+
6
10
  const BRC74Hex =
7
11
  'fe8a6a0c000c04fde80b0011774f01d26412f0d16ea3f0447be0b5ebec67b0782e321a7a01cbdf7f734e30fde90b02004e53753e3fe4667073063a17987292cfdea278824e9888e52180581d7188d8fdea0b025e441996fc53f0191d649e68a200e752fb5f39e0d5617083408fa179ddc5c998fdeb0b0102fdf405000671394f72237d08a4277f4435e5b6edf7adc272f25effef27cdfe805ce71a81fdf50500262bccabec6c4af3ed00cc7a7414edea9c5efa92fb8623dd6160a001450a528201fdfb020101fd7c010093b3efca9b77ddec914f8effac691ecb54e2c81d0ab81cbc4c4b93befe418e8501bf01015e005881826eb6973c54003a02118fe270f03d46d02681c8bc71cd44c613e86302f8012e00e07a2bb8bb75e5accff266022e1e5e6e7b4d6d943a04faadcf2ab4a22f796ff30116008120cafa17309c0bb0e0ffce835286b3a2dcae48e4497ae2d2b7ced4f051507d010a00502e59ac92f46543c23006bff855d96f5e648043f0fb87a7a5949e6a9bebae430104001ccd9f8f64f4d0489b30cc815351cf425e0e78ad79a589350e4341ac165dbe45010301010000af8764ce7e1cc132ab5ed2229a005c87201c9a5ee15c0f91dd53eff31ab30cd4'
8
12
 
@@ -131,6 +135,26 @@ class FakeChainTracker implements ChainTracker {
131
135
  }
132
136
  }
133
137
 
138
+ /** Splits BRC74JSON into two partial paths (A covers txid2, B covers txid3) ready to combine. */
139
+ function buildSplitPaths (): [MerklePath, MerklePath] {
140
+ const path0A = [...BRC74JSON.path[0]]
141
+ const path0B = [...BRC74JSON.path[0]]
142
+ const path1A = [...BRC74JSON.path[1]]
143
+ const path1B = [...BRC74JSON.path[1]]
144
+ const pathRest = [...BRC74JSON.path]
145
+ pathRest.shift()
146
+ pathRest.shift()
147
+ path0A.splice(2, 2)
148
+ path0B.shift()
149
+ path0B.shift()
150
+ path1A.shift()
151
+ path1B.pop()
152
+ return [
153
+ new MerklePath(BRC74JSON.blockHeight, [path0A, path1A, ...pathRest]),
154
+ new MerklePath(BRC74JSON.blockHeight, [path0B, path1B, ...pathRest])
155
+ ]
156
+ }
157
+
134
158
  describe('MerklePath', () => {
135
159
  it('Parses from hex', () => {
136
160
  const path = MerklePath.fromHex(BRC74Hex)
@@ -163,28 +187,7 @@ describe('MerklePath', () => {
163
187
  )
164
188
  })
165
189
  it('Combines two paths', () => {
166
- const path0A = [...BRC74JSON.path[0]]
167
- const path0B = [...BRC74JSON.path[0]]
168
- const path1A = [...BRC74JSON.path[1]]
169
- const path1B = [...BRC74JSON.path[1]]
170
- const pathRest = [...BRC74JSON.path]
171
- pathRest.shift()
172
- pathRest.shift()
173
- path0A.splice(2, 2)
174
- path0B.shift()
175
- path0B.shift()
176
- path1A.shift()
177
- path1B.pop()
178
- const pathAJSON = {
179
- blockHeight: BRC74JSON.blockHeight,
180
- path: [path0A, path1A, ...pathRest]
181
- }
182
- const pathBJSON = {
183
- blockHeight: BRC74JSON.blockHeight,
184
- path: [path0B, path1B, ...pathRest]
185
- }
186
- const pathA = new MerklePath(pathAJSON.blockHeight, pathAJSON.path)
187
- const pathB = new MerklePath(pathBJSON.blockHeight, pathBJSON.path)
190
+ const [pathA, pathB] = buildSplitPaths()
188
191
  expect(pathA.computeRoot(BRC74TXID2)).toEqual(BRC74Root)
189
192
  expect(() => pathA.computeRoot(BRC74TXID3)).toThrow()
190
193
  expect(() => pathB.computeRoot(BRC74TXID2)).toThrow()
@@ -194,6 +197,41 @@ describe('MerklePath', () => {
194
197
  expect(pathA.computeRoot(BRC74TXID2)).toEqual(BRC74Root)
195
198
  expect(pathA.computeRoot(BRC74TXID3)).toEqual(BRC74Root)
196
199
  })
200
+ it('Serializes and deserializes a combined trimmed path', () => {
201
+ const [pathA, pathB] = buildSplitPaths()
202
+ 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)
207
+ })
208
+ it('Constructs a compound path from all txids at level 0 only', () => {
209
+ // A single-level compound path: all txids for a block given at level 0, no higher levels.
210
+ // The implementation should be able to compute the merkle root by calculating up from the leaves.
211
+ const tx0 = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
212
+ const tx1 = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
213
+ const tx2 = 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
214
+ const tx3 = 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'
215
+ 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)
229
+ // 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)
234
+ })
197
235
  it('Rejects invalid bumps', () => {
198
236
  for (const invalid of invalidBumps) {
199
237
  expect(() => MerklePath.fromHex(invalid.bump)).toThrow(invalid.error)