@bsv/sdk 2.0.10 → 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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/overlay-tools/HostReputationTracker.js +21 -13
- package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
- package/dist/cjs/src/primitives/PrivateKey.js +3 -3
- package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
- package/dist/cjs/src/script/Spend.js +17 -9
- package/dist/cjs/src/script/Spend.js.map +1 -1
- package/dist/cjs/src/storage/StorageDownloader.js +6 -6
- package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
- package/dist/cjs/src/storage/StorageUtils.js +1 -1
- package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
- package/dist/cjs/src/transaction/MerklePath.js +45 -31
- package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/overlay-tools/HostReputationTracker.js +21 -13
- package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
- package/dist/esm/src/primitives/PrivateKey.js +3 -3
- package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
- package/dist/esm/src/script/Spend.js +17 -9
- package/dist/esm/src/script/Spend.js.map +1 -1
- package/dist/esm/src/storage/StorageDownloader.js +6 -6
- package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
- package/dist/esm/src/storage/StorageUtils.js +1 -1
- package/dist/esm/src/storage/StorageUtils.js.map +1 -1
- package/dist/esm/src/transaction/MerklePath.js +45 -31
- package/dist/esm/src/transaction/MerklePath.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/overlay-tools/HostReputationTracker.d.ts.map +1 -1
- package/dist/types/src/script/Spend.d.ts.map +1 -1
- package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/utils/__tests/validateCertificates.test.ts +12 -9
- package/src/kvstore/__tests/LocalKVStore.test.ts +4 -6
- package/src/overlay-tools/HostReputationTracker.ts +17 -14
- package/src/primitives/PrivateKey.ts +3 -3
- package/src/script/Spend.ts +19 -11
- package/src/storage/StorageDownloader.ts +6 -6
- package/src/storage/StorageUtils.ts +1 -1
- package/src/transaction/MerklePath.ts +51 -42
- package/src/transaction/__tests/MerklePath.test.ts +191 -22
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { validateCertificates } from '../../../auth/utils/validateCertificates'
|
|
2
2
|
import { VerifiableCertificate } from '../../../auth/certificates/VerifiableCertificate'
|
|
3
|
-
import { ProtoWallet } from '../../../wallet/index'
|
|
3
|
+
import { ProtoWallet, WalletInterface } from '../../../wallet/index'
|
|
4
4
|
import { PrivateKey } from '../../../primitives/index'
|
|
5
|
+
import { AuthMessage } from '../../../auth/types'
|
|
5
6
|
|
|
6
7
|
let mockVerify = jest.fn(async () => await Promise.resolve(true))
|
|
7
8
|
let mockDecryptFields = jest.fn(
|
|
@@ -40,8 +41,8 @@ jest.mock('../../../auth/certificates/VerifiableCertificate', () => {
|
|
|
40
41
|
})
|
|
41
42
|
|
|
42
43
|
describe('validateCertificates', () => {
|
|
43
|
-
let verifierWallet
|
|
44
|
-
let message
|
|
44
|
+
let verifierWallet: WalletInterface
|
|
45
|
+
let message: AuthMessage
|
|
45
46
|
|
|
46
47
|
beforeEach(() => {
|
|
47
48
|
jest.clearAllMocks()
|
|
@@ -53,8 +54,10 @@ describe('validateCertificates', () => {
|
|
|
53
54
|
async () => await Promise.resolve({ field1: 'decryptedValue1' })
|
|
54
55
|
)
|
|
55
56
|
|
|
56
|
-
verifierWallet = new ProtoWallet(new PrivateKey(1))
|
|
57
|
+
verifierWallet = new ProtoWallet(new PrivateKey(1)) as unknown as WalletInterface
|
|
57
58
|
message = {
|
|
59
|
+
version: '1.0',
|
|
60
|
+
messageType: 'certificateResponse',
|
|
58
61
|
identityKey: 'valid_subject',
|
|
59
62
|
certificates: [
|
|
60
63
|
{
|
|
@@ -65,7 +68,7 @@ describe('validateCertificates', () => {
|
|
|
65
68
|
revocationOutpoint: 'outpoint',
|
|
66
69
|
fields: { field1: 'encryptedData1' },
|
|
67
70
|
decryptedFields: {}
|
|
68
|
-
}
|
|
71
|
+
} as any
|
|
69
72
|
]
|
|
70
73
|
}
|
|
71
74
|
})
|
|
@@ -76,9 +79,9 @@ describe('validateCertificates', () => {
|
|
|
76
79
|
).resolves.not.toThrow()
|
|
77
80
|
|
|
78
81
|
expect(VerifiableCertificate).toHaveBeenCalledTimes(
|
|
79
|
-
message.certificates
|
|
82
|
+
message.certificates!.length
|
|
80
83
|
)
|
|
81
|
-
expect(mockVerify).toHaveBeenCalledTimes(message.certificates
|
|
84
|
+
expect(mockVerify).toHaveBeenCalledTimes(message.certificates!.length)
|
|
82
85
|
expect(mockDecryptFields).toHaveBeenCalledWith(verifierWallet, undefined, undefined, undefined)
|
|
83
86
|
})
|
|
84
87
|
|
|
@@ -147,9 +150,9 @@ describe('validateCertificates', () => {
|
|
|
147
150
|
revocationOutpoint: 'outpoint',
|
|
148
151
|
fields: { field1: 'encryptedData1' },
|
|
149
152
|
decryptedFields: {}
|
|
150
|
-
}
|
|
153
|
+
} as any
|
|
151
154
|
|
|
152
|
-
message.certificates
|
|
155
|
+
message.certificates!.push(anotherCertificate)
|
|
153
156
|
|
|
154
157
|
await expect(
|
|
155
158
|
validateCertificates(verifierWallet, message)
|
|
@@ -6,13 +6,11 @@ import * as Utils from '../../primitives/utils.js'
|
|
|
6
6
|
import {
|
|
7
7
|
WalletInterface,
|
|
8
8
|
ListOutputsResult,
|
|
9
|
-
WalletDecryptResult,
|
|
10
9
|
WalletEncryptResult,
|
|
11
10
|
CreateActionResult,
|
|
12
11
|
SignActionResult
|
|
13
12
|
} from '../../wallet/Wallet.interfaces.js'
|
|
14
13
|
import Transaction from '../../transaction/Transaction.js'
|
|
15
|
-
import { Beef } from '../../transaction/Beef.js'
|
|
16
14
|
import { mock } from 'node:test'
|
|
17
15
|
|
|
18
16
|
// --- Constants for Mock Values ---
|
|
@@ -303,7 +301,7 @@ describe('localKVStore', () => {
|
|
|
303
301
|
const existingOutput = { outpoint: existingOutpoint, txid: 'oldTxId', vout: 0, lockingScript: 'oldScriptHex' } // Added script
|
|
304
302
|
const mockBEEF = [1, 2, 3, 4, 5, 6]
|
|
305
303
|
const signableRef = 'signableTxRef123'
|
|
306
|
-
const signableTx = []
|
|
304
|
+
const signableTx: any[] = []
|
|
307
305
|
const updatedTxId = 'updatedTxId'
|
|
308
306
|
|
|
309
307
|
const valueArray = Array.from(testRawValueBuffer)
|
|
@@ -397,7 +395,7 @@ describe('localKVStore', () => {
|
|
|
397
395
|
const existingOutput2 = { outpoint: existingOutpoint2, txid: 'oldTxId2', vout: 1, lockingScript: 's2' }
|
|
398
396
|
const mockBEEF = [1, 2, 3, 4, 5, 6]
|
|
399
397
|
const signableRef = 'signableTxRefMulti'
|
|
400
|
-
const signableTx = []
|
|
398
|
+
const signableTx: any[] = []
|
|
401
399
|
const updatedTxId = 'updatedTxIdMulti'
|
|
402
400
|
const mockTxObject = {} // Dummy TX object
|
|
403
401
|
|
|
@@ -564,7 +562,7 @@ describe('localKVStore', () => {
|
|
|
564
562
|
const existingOutput2 = { outpoint: existingOutpoint2, txid: 'removeTxId2', vout: 1, lockingScript: 's2' }
|
|
565
563
|
const mockBEEF = Buffer.from('mockBEEFRemove')
|
|
566
564
|
const signableRef = 'signableTxRefRemove'
|
|
567
|
-
const signableTx = []
|
|
565
|
+
const signableTx: any[] = []
|
|
568
566
|
const removalTxId = 'removalTxId'
|
|
569
567
|
const mockTxObject = {}
|
|
570
568
|
|
|
@@ -628,7 +626,7 @@ describe('localKVStore', () => {
|
|
|
628
626
|
const existingOutput1 = { outpoint: existingOutpoint1, txid: 'failRemoveTxId1', vout: 0, lockingScript: 's1' }
|
|
629
627
|
const mockBEEF = Buffer.from('mockBEEFFailRemove')
|
|
630
628
|
const signableRef = 'signableTxRefFailRemove'
|
|
631
|
-
const signableTx = []
|
|
629
|
+
const signableTx: any[] = []
|
|
632
630
|
const mockTxObject = {}
|
|
633
631
|
|
|
634
632
|
mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput1], totalOutputs: 1, BEEF: mockBEEF } as any)
|
|
@@ -67,12 +67,14 @@ export class HostReputationTracker {
|
|
|
67
67
|
const now = Date.now()
|
|
68
68
|
entry.totalFailures += 1
|
|
69
69
|
entry.consecutiveFailures += 1
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
let msg: string | undefined
|
|
71
|
+
if (typeof reason === 'string') {
|
|
72
|
+
msg = reason
|
|
73
|
+
} else if (reason instanceof Error) {
|
|
74
|
+
msg = reason.message
|
|
75
|
+
} else {
|
|
76
|
+
msg = undefined
|
|
77
|
+
}
|
|
76
78
|
const immediate =
|
|
77
79
|
typeof msg === 'string' &&
|
|
78
80
|
(msg.includes('ERR_NAME_NOT_RESOLVED') ||
|
|
@@ -93,12 +95,13 @@ export class HostReputationTracker {
|
|
|
93
95
|
entry.backoffUntil = now + backoffDuration
|
|
94
96
|
}
|
|
95
97
|
entry.lastUpdatedAt = now
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
if (typeof reason === 'string') {
|
|
99
|
+
entry.lastError = reason
|
|
100
|
+
} else if (reason instanceof Error) {
|
|
101
|
+
entry.lastError = reason.message
|
|
102
|
+
} else {
|
|
103
|
+
entry.lastError = undefined
|
|
104
|
+
}
|
|
102
105
|
this.saveToStorage()
|
|
103
106
|
}
|
|
104
107
|
|
|
@@ -133,13 +136,13 @@ export class HostReputationTracker {
|
|
|
133
136
|
|
|
134
137
|
snapshot (host: string): HostReputationEntry | undefined {
|
|
135
138
|
const entry = this.stats.get(host)
|
|
136
|
-
return entry
|
|
139
|
+
return entry == null ? undefined : { ...entry }
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
private getStorage (): any {
|
|
140
143
|
try {
|
|
141
144
|
const g: any = typeof globalThis === 'object' ? globalThis : undefined
|
|
142
|
-
if (g
|
|
145
|
+
if (g?.localStorage == null) return undefined
|
|
143
146
|
return g.localStorage
|
|
144
147
|
} catch {
|
|
145
148
|
return undefined
|
|
@@ -55,7 +55,7 @@ export class KeyShares {
|
|
|
55
55
|
const [x, y, t, i] = shareParts
|
|
56
56
|
if (t === undefined) throw new Error('Threshold not found in share ' + idx.toString())
|
|
57
57
|
if (i === undefined) throw new Error('Integrity not found in share ' + idx.toString())
|
|
58
|
-
const tInt = parseInt(t)
|
|
58
|
+
const tInt = Number.parseInt(t, 10)
|
|
59
59
|
if (idx !== 0 && threshold !== tInt) { throw new Error('Threshold mismatch in share ' + idx.toString()) }
|
|
60
60
|
if (idx !== 0 && integrity !== i) { throw new Error('Integrity mismatch in share ' + idx.toString()) }
|
|
61
61
|
threshold = tInt
|
|
@@ -399,7 +399,7 @@ export default class PrivateKey extends BigNumber {
|
|
|
399
399
|
let sharedSecret: Point
|
|
400
400
|
if (typeof retrieveCachedSharedSecret === 'function') {
|
|
401
401
|
const retrieved = retrieveCachedSharedSecret(this, publicKey)
|
|
402
|
-
if (
|
|
402
|
+
if (retrieved !== undefined) {
|
|
403
403
|
sharedSecret = retrieved
|
|
404
404
|
} else {
|
|
405
405
|
sharedSecret = this.deriveSharedSecret(publicKey)
|
|
@@ -429,7 +429,7 @@ export default class PrivateKey extends BigNumber {
|
|
|
429
429
|
* const shares = key.toKeyShares(2, 5)
|
|
430
430
|
*/
|
|
431
431
|
toKeyShares (threshold: number, totalShares: number): KeyShares {
|
|
432
|
-
if (typeof threshold !== 'number' || typeof totalShares !== 'number') { throw new
|
|
432
|
+
if (typeof threshold !== 'number' || typeof totalShares !== 'number') { throw new TypeError('threshold and totalShares must be numbers') }
|
|
433
433
|
if (threshold < 2) throw new Error('threshold must be at least 2')
|
|
434
434
|
if (totalShares < 2) throw new Error('totalShares must be at least 2')
|
|
435
435
|
if (threshold > totalShares) { throw new Error('threshold should be less than or equal to totalShares') }
|
package/src/script/Spend.ts
CHANGED
|
@@ -266,7 +266,11 @@ export default class Spend {
|
|
|
266
266
|
if (this.stack.length === 0) {
|
|
267
267
|
this.scriptEvaluationError('Attempted to pop from an empty stack.')
|
|
268
268
|
}
|
|
269
|
-
const item = this.stack.pop()
|
|
269
|
+
const item = this.stack.pop()
|
|
270
|
+
if (item === undefined) {
|
|
271
|
+
this.scriptEvaluationError('Attempted to pop from an empty stack.')
|
|
272
|
+
return [] // unreachable; scriptEvaluationError always throws
|
|
273
|
+
}
|
|
270
274
|
this.stackMem -= item.length
|
|
271
275
|
return item
|
|
272
276
|
}
|
|
@@ -290,7 +294,11 @@ export default class Spend {
|
|
|
290
294
|
if (this.altStack.length === 0) {
|
|
291
295
|
this.scriptEvaluationError('Attempted to pop from an empty alt stack.')
|
|
292
296
|
}
|
|
293
|
-
const item = this.altStack.pop()
|
|
297
|
+
const item = this.altStack.pop()
|
|
298
|
+
if (item === undefined) {
|
|
299
|
+
this.scriptEvaluationError('Attempted to pop from an empty alt stack.')
|
|
300
|
+
return [] // unreachable; scriptEvaluationError always throws
|
|
301
|
+
}
|
|
294
302
|
this.altStackMem -= item.length
|
|
295
303
|
return item
|
|
296
304
|
}
|
|
@@ -308,7 +316,7 @@ export default class Spend {
|
|
|
308
316
|
this.scriptEvaluationError('The signature must have a low S value.')
|
|
309
317
|
return false
|
|
310
318
|
}
|
|
311
|
-
} catch
|
|
319
|
+
} catch {
|
|
312
320
|
this.scriptEvaluationError('The signature format is invalid.')
|
|
313
321
|
return false
|
|
314
322
|
}
|
|
@@ -340,7 +348,7 @@ export default class Spend {
|
|
|
340
348
|
}
|
|
341
349
|
try {
|
|
342
350
|
PublicKey.fromDER(buf as number[]) // This can throw for stricter DER rules
|
|
343
|
-
} catch
|
|
351
|
+
} catch {
|
|
344
352
|
this.scriptEvaluationError('The public key is in an unknown format.')
|
|
345
353
|
return false
|
|
346
354
|
}
|
|
@@ -395,7 +403,7 @@ export default class Spend {
|
|
|
395
403
|
const operation = currentScript.chunks[this.programCounter]
|
|
396
404
|
|
|
397
405
|
const currentOpcode = operation.op
|
|
398
|
-
if (
|
|
406
|
+
if (currentOpcode === undefined) {
|
|
399
407
|
this.scriptEvaluationError(`Missing opcode in ${this.context} at pc=${this.programCounter}.`) // Error thrown
|
|
400
408
|
}
|
|
401
409
|
if (Array.isArray(operation.data) && operation.data.length > maxScriptElementSize) {
|
|
@@ -547,7 +555,7 @@ export default class Spend {
|
|
|
547
555
|
break
|
|
548
556
|
case OP.OP_ELSE:
|
|
549
557
|
if (this.ifStack.length === 0) this.scriptEvaluationError('OP_ELSE requires a preceeding OP_IF.')
|
|
550
|
-
this.ifStack[this.ifStack.length - 1] = !this.ifStack[this.ifStack.length - 1]
|
|
558
|
+
this.ifStack[this.ifStack.length - 1] = !(this.ifStack[this.ifStack.length - 1])
|
|
551
559
|
break
|
|
552
560
|
case OP.OP_ENDIF:
|
|
553
561
|
if (this.ifStack.length === 0) this.scriptEvaluationError('OP_ENDIF requires a preceeding OP_IF.')
|
|
@@ -682,7 +690,7 @@ export default class Spend {
|
|
|
682
690
|
// stack is [... rest, x1, x2]
|
|
683
691
|
// We want [... rest, x2_copy, x1, x2]
|
|
684
692
|
this.ensureStackMem(buf1.length)
|
|
685
|
-
this.stack.splice(
|
|
693
|
+
this.stack.splice(-2, 0, buf1.slice()) // Insert copy of x2 before x1
|
|
686
694
|
this.stackMem += buf1.length // Account for the new copy
|
|
687
695
|
break
|
|
688
696
|
case OP.OP_SIZE:
|
|
@@ -769,7 +777,7 @@ export default class Spend {
|
|
|
769
777
|
case OP.OP_NEGATE: bn = bn.neg(); break
|
|
770
778
|
case OP.OP_ABS: if (bn.isNeg()) bn = bn.neg(); break
|
|
771
779
|
case OP.OP_NOT: bn = new BigNumber(bn.cmpn(0) === 0 ? 1 : 0); break
|
|
772
|
-
case OP.OP_0NOTEQUAL: bn = new BigNumber(bn.cmpn(0)
|
|
780
|
+
case OP.OP_0NOTEQUAL: bn = new BigNumber(bn.cmpn(0) === 0 ? 0 : 1); break
|
|
773
781
|
}
|
|
774
782
|
this.pushStack(bn.toScriptNum())
|
|
775
783
|
break
|
|
@@ -812,7 +820,7 @@ export default class Spend {
|
|
|
812
820
|
case OP.OP_BOOLOR: resultBnArithmetic = new BigNumber((bn1.cmpn(0) !== 0 || bn2.cmpn(0) !== 0) ? 1 : 0); break
|
|
813
821
|
case OP.OP_NUMEQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) === 0 ? 1 : 0); break
|
|
814
822
|
case OP.OP_NUMEQUALVERIFY: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) === 0 ? 1 : 0); break
|
|
815
|
-
case OP.OP_NUMNOTEQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2)
|
|
823
|
+
case OP.OP_NUMNOTEQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) === 0 ? 0 : 1); break
|
|
816
824
|
case OP.OP_LESSTHAN: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) < 0 ? 1 : 0); break
|
|
817
825
|
case OP.OP_GREATERTHAN: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) > 0 ? 1 : 0); break
|
|
818
826
|
case OP.OP_LESSTHANOREQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) <= 0 ? 1 : 0); break
|
|
@@ -875,7 +883,7 @@ export default class Spend {
|
|
|
875
883
|
|
|
876
884
|
pubkey = PublicKey.fromDER(bufPubkey)
|
|
877
885
|
fSuccess = this.verifySignature(sig, pubkey, subscript)
|
|
878
|
-
} catch
|
|
886
|
+
} catch {
|
|
879
887
|
fSuccess = false
|
|
880
888
|
}
|
|
881
889
|
}
|
|
@@ -949,7 +957,7 @@ export default class Spend {
|
|
|
949
957
|
sig = TransactionSignature.fromChecksigFormat(bufSig)
|
|
950
958
|
pubkey = PublicKey.fromDER(bufPubkey)
|
|
951
959
|
fOk = this.verifySignature(sig, pubkey, subscript)
|
|
952
|
-
} catch
|
|
960
|
+
} catch {
|
|
953
961
|
fOk = false
|
|
954
962
|
}
|
|
955
963
|
}
|
|
@@ -35,9 +35,9 @@ export class StorageDownloader {
|
|
|
35
35
|
}
|
|
36
36
|
const decodedResults: string[] = []
|
|
37
37
|
const currentTime = Math.floor(Date.now() / 1000)
|
|
38
|
-
for (
|
|
39
|
-
const tx = Transaction.fromBEEF(
|
|
40
|
-
const { fields } = PushDrop.decode(tx.outputs[
|
|
38
|
+
for (const output of response.outputs) {
|
|
39
|
+
const tx = Transaction.fromBEEF(output.beef)
|
|
40
|
+
const { fields } = PushDrop.decode(tx.outputs[output.outputIndex].lockingScript)
|
|
41
41
|
|
|
42
42
|
const expiryTime = new Utils.Reader(fields[3]).readVarIntNum()
|
|
43
43
|
if (expiryTime < currentTime) {
|
|
@@ -66,10 +66,10 @@ export class StorageDownloader {
|
|
|
66
66
|
throw new Error('No one currently hosts this file!')
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
for (
|
|
69
|
+
for (const url of downloadURLs) {
|
|
70
70
|
try {
|
|
71
71
|
// The url is fetched
|
|
72
|
-
const result = await fetch(
|
|
72
|
+
const result = await fetch(url, { method: 'GET' })
|
|
73
73
|
|
|
74
74
|
// If the request fails, continue to the next url
|
|
75
75
|
if (!result.ok || result.status >= 400 || result.body == null) {
|
|
@@ -105,7 +105,7 @@ export class StorageDownloader {
|
|
|
105
105
|
data,
|
|
106
106
|
mimeType: result.headers.get('Content-Type')
|
|
107
107
|
}
|
|
108
|
-
} catch
|
|
108
|
+
} catch {
|
|
109
109
|
continue
|
|
110
110
|
}
|
|
111
111
|
}
|
|
@@ -61,7 +61,7 @@ export default class MerklePath {
|
|
|
61
61
|
const blockHeight = reader.readVarIntNum()
|
|
62
62
|
const treeHeight = reader.readUInt8()
|
|
63
63
|
// Explicitly define the type of path as an array of arrays of leaf objects
|
|
64
|
-
const path: Array<Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }>> = Array(treeHeight)
|
|
64
|
+
const path: Array<Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }>> = new Array(treeHeight)
|
|
65
65
|
.fill(null)
|
|
66
66
|
.map(() => [])
|
|
67
67
|
let flags: number, offset: number, nLeavesAtThisHeight: number
|
|
@@ -76,7 +76,7 @@ export default class MerklePath {
|
|
|
76
76
|
txid?: boolean
|
|
77
77
|
duplicate?: boolean
|
|
78
78
|
} = { offset }
|
|
79
|
-
if ((flags & 1)
|
|
79
|
+
if ((flags & 1) === 1) {
|
|
80
80
|
leaf.duplicate = true
|
|
81
81
|
} else {
|
|
82
82
|
if ((flags & 2) !== 0) {
|
|
@@ -140,7 +140,7 @@ export default class MerklePath {
|
|
|
140
140
|
this.path = path
|
|
141
141
|
|
|
142
142
|
// store all of the legal offsets which we expect given the txid indices.
|
|
143
|
-
const legalOffsets = Array(this.path.length)
|
|
143
|
+
const legalOffsets = new Array(this.path.length)
|
|
144
144
|
.fill(0)
|
|
145
145
|
.map(() => new Set())
|
|
146
146
|
this.path.forEach((leaves, height) => {
|
|
@@ -161,12 +161,10 @@ export default class MerklePath {
|
|
|
161
161
|
legalOffsets[h].add((leaf.offset >> h) ^ 1)
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
-
} else {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
)
|
|
169
|
-
}
|
|
164
|
+
} else if (legalOffsetsOnly && !legalOffsets[height].has(leaf.offset)) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Invalid offset: ${leaf.offset}, at height: ${height}, with legal offsets: ${Array.from(legalOffsets[height]).join(', ')}`
|
|
167
|
+
)
|
|
170
168
|
}
|
|
171
169
|
})
|
|
172
170
|
})
|
|
@@ -260,19 +258,16 @@ export default class MerklePath {
|
|
|
260
258
|
computeRoot (txid?: string): string {
|
|
261
259
|
if (typeof txid !== 'string') {
|
|
262
260
|
const foundLeaf = this.path[0].find((leaf) => Boolean(leaf?.hash))
|
|
263
|
-
if (foundLeaf
|
|
261
|
+
if (foundLeaf == null) {
|
|
264
262
|
throw new Error('No valid leaf found in the Merkle Path')
|
|
265
263
|
}
|
|
266
264
|
txid = foundLeaf.hash
|
|
267
265
|
}
|
|
268
266
|
// Find the index of the txid at the lowest level of the Merkle tree
|
|
269
267
|
if (typeof txid !== 'string') {
|
|
270
|
-
throw new
|
|
268
|
+
throw new TypeError('Transaction ID is undefined')
|
|
271
269
|
}
|
|
272
270
|
const index = this.indexOf(txid)
|
|
273
|
-
if (typeof index !== 'number') {
|
|
274
|
-
throw new Error(`This proof does not contain the txid: ${txid ?? 'undefined'}`)
|
|
275
|
-
}
|
|
276
271
|
// Calculate the root using the index as a way to determine which direction to concatenate.
|
|
277
272
|
const hash = (m: string): string =>
|
|
278
273
|
toHex(hash256(toArray(m, 'hex').reverse()).reverse())
|
|
@@ -281,17 +276,26 @@ export default class MerklePath {
|
|
|
281
276
|
// special case for blocks with only one transaction
|
|
282
277
|
if (this.path.length === 1 && this.path[0].length === 1) return workingHash
|
|
283
278
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
279
|
+
// Determine effective tree height. For a compound path where all txids are at level 0
|
|
280
|
+
// (path.length === 1 or intermediate levels are empty/trimmed), we need to compute up
|
|
281
|
+
// to the height implied by the highest offset present in path[0].
|
|
282
|
+
const maxOffset = this.path[0].reduce((max, l) => Math.max(max, l.offset), 0)
|
|
283
|
+
const treeHeight = Math.max(this.path.length, 32 - Math.clz32(maxOffset))
|
|
284
|
+
|
|
285
|
+
for (let height = 0; height < treeHeight; height++) {
|
|
287
286
|
const offset = (index >> height) ^ 1
|
|
288
287
|
const leaf = this.findOrComputeLeaf(height, offset)
|
|
289
288
|
if (typeof leaf !== 'object') {
|
|
289
|
+
// For single-level paths (all txids at level 0), the sibling may be beyond the tree
|
|
290
|
+
// because this is the last odd node at this height. Bitcoin Merkle duplicates it.
|
|
291
|
+
if (this.path.length === 1 && (index >> height) === (maxOffset >> height)) {
|
|
292
|
+
workingHash = hash((workingHash ?? '') + (workingHash ?? ''))
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
290
295
|
throw new Error(`Missing hash for index ${index} at height ${height}`)
|
|
291
|
-
}
|
|
292
|
-
if (leaf.duplicate === true) {
|
|
296
|
+
} else if (leaf.duplicate === true) {
|
|
293
297
|
workingHash = hash((workingHash ?? '') + (workingHash ?? ''))
|
|
294
|
-
} else if (offset % 2
|
|
298
|
+
} else if (offset % 2 === 1) {
|
|
295
299
|
workingHash = hash((leaf.hash ?? '') + (workingHash ?? ''))
|
|
296
300
|
} else {
|
|
297
301
|
workingHash = hash((workingHash ?? '') + (leaf.hash ?? ''))
|
|
@@ -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
|
|
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
|
|
|
@@ -330,7 +334,20 @@ export default class MerklePath {
|
|
|
330
334
|
if (leaf0 == null || leaf0.hash == null || leaf0.hash === '') return undefined
|
|
331
335
|
|
|
332
336
|
const leaf1 = this.findOrComputeLeaf(h, l + 1)
|
|
333
|
-
if (leaf1 == null)
|
|
337
|
+
if (leaf1?.hash == null) {
|
|
338
|
+
// Explicit duplicate marker — duplicate leaf0 regardless of path depth.
|
|
339
|
+
if (leaf1?.duplicate === true) {
|
|
340
|
+
return { offset, hash: hash(leaf0.hash + leaf0.hash) }
|
|
341
|
+
}
|
|
342
|
+
// For single-level paths, leaf0 may be the last odd node at height h — duplicate it.
|
|
343
|
+
if (this.path.length === 1) {
|
|
344
|
+
const maxOffset0 = this.path[0].reduce((max, lf) => Math.max(max, lf.offset), 0)
|
|
345
|
+
if (l === (maxOffset0 >> h)) {
|
|
346
|
+
return { offset, hash: hash(leaf0.hash + leaf0.hash) }
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return undefined
|
|
350
|
+
}
|
|
334
351
|
|
|
335
352
|
let workinghash: string
|
|
336
353
|
if (leaf1.duplicate === true) {
|
|
@@ -388,26 +405,18 @@ export default class MerklePath {
|
|
|
388
405
|
const combinedPath: Array<Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }>> = []
|
|
389
406
|
for (let h = 0; h < this.path.length; h++) {
|
|
390
407
|
combinedPath.push([])
|
|
391
|
-
for (
|
|
392
|
-
combinedPath[h].push(
|
|
408
|
+
for (const leaf of this.path[h]) {
|
|
409
|
+
combinedPath[h].push(leaf)
|
|
393
410
|
}
|
|
394
|
-
for (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
} else {
|
|
411
|
+
for (const otherLeaf of other.path[h]) {
|
|
412
|
+
const existingLeaf = combinedPath[h].find(
|
|
413
|
+
(leaf) => leaf.offset === otherLeaf.offset
|
|
414
|
+
)
|
|
415
|
+
if (existingLeaf === undefined) {
|
|
416
|
+
combinedPath[h].push(otherLeaf)
|
|
417
|
+
} else if (otherLeaf?.txid !== undefined && otherLeaf?.txid !== null) {
|
|
402
418
|
// Ensure that any elements which appear in both are not downgraded to a non txid.
|
|
403
|
-
|
|
404
|
-
const target = combinedPath[h].find(
|
|
405
|
-
(leaf) => leaf.offset === other.path[h][l].offset
|
|
406
|
-
)
|
|
407
|
-
if (target !== null && target !== undefined) {
|
|
408
|
-
target.txid = true
|
|
409
|
-
}
|
|
410
|
-
}
|
|
419
|
+
existingLeaf.txid = true
|
|
411
420
|
}
|
|
412
421
|
}
|
|
413
422
|
}
|