@bsv/sdk 2.0.12 → 2.0.13
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/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/cjs/src/transaction/MerklePath.js +132 -0
- package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/esm/src/transaction/MerklePath.js +132 -0
- package/dist/esm/src/transaction/MerklePath.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
- package/dist/types/src/transaction/MerklePath.d.ts +27 -0
- package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/storage.md +1 -1
- package/docs/reference/transaction.md +40 -0
- package/package.json +1 -1
- package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
- package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
- package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
- package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
- package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
- package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
- package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
- package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
- package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
- package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
- package/src/primitives/__tests/Curve.additional.test.ts +208 -0
- package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
- package/src/primitives/__tests/Hash.additional.test.ts +59 -0
- package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
- package/src/primitives/__tests/Point.additional.test.ts +503 -0
- package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
- package/src/primitives/__tests/Random.additional.test.ts +262 -0
- package/src/primitives/__tests/Signature.test.ts +333 -0
- package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
- package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
- package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
- package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
- package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
- package/src/script/__tests/Script.additional.test.ts +100 -0
- package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
- package/src/script/__tests/Spend.additional.test.ts +837 -0
- package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
- package/src/transaction/MerklePath.ts +155 -0
- package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
- package/src/transaction/__tests/Broadcaster.test.ts +159 -0
- package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
- package/src/transaction/__tests/MerklePath.test.ts +80 -0
- package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
- package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
- package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
- package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
- package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
- package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
- package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
- package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
- package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
- package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
- package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
- package/src/wallet/__tests/WERR.test.ts +212 -0
- package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
- package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
- package/src/wallet/__tests/WalletError.test.ts +290 -0
- package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
- package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
- package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import RPuzzle from '../RPuzzle'
|
|
2
|
+
import Spend from '../../Spend'
|
|
3
|
+
import LockingScript from '../../LockingScript'
|
|
4
|
+
import Transaction from '../../../transaction/Transaction'
|
|
5
|
+
import PrivateKey from '../../../primitives/PrivateKey'
|
|
6
|
+
import BigNumber from '../../../primitives/BigNumber'
|
|
7
|
+
import Curve from '../../../primitives/Curve'
|
|
8
|
+
|
|
9
|
+
const ZERO_TXID = '0'.repeat(64)
|
|
10
|
+
|
|
11
|
+
function getRValue (k: BigNumber): number[] {
|
|
12
|
+
const c = new Curve()
|
|
13
|
+
let r = c.g.mul(k).x?.umod(c.n)?.toArray()
|
|
14
|
+
if (r == null) return []
|
|
15
|
+
if (r[0] > 127) r = [0, ...r]
|
|
16
|
+
return r
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function buildRPuzzleSpend (
|
|
20
|
+
puz: RPuzzle,
|
|
21
|
+
k: BigNumber,
|
|
22
|
+
privateKey: PrivateKey,
|
|
23
|
+
r: number[],
|
|
24
|
+
signOutputs: 'all' | 'none' | 'single' = 'all',
|
|
25
|
+
anyoneCanPay: boolean = false
|
|
26
|
+
): Promise<Spend> {
|
|
27
|
+
const lockingScript: LockingScript = puz.lock(r)
|
|
28
|
+
const sourceTx = new Transaction(1, [], [{ lockingScript, satoshis: 1 }], 0)
|
|
29
|
+
const spendTx = new Transaction(
|
|
30
|
+
1,
|
|
31
|
+
[{ sourceTransaction: sourceTx, sourceOutputIndex: 0, sequence: 0xffffffff }],
|
|
32
|
+
[{ lockingScript: LockingScript.fromASM('OP_1'), satoshis: 1 }],
|
|
33
|
+
0
|
|
34
|
+
)
|
|
35
|
+
const unlockingScript = await puz.unlock(k, privateKey, signOutputs, anyoneCanPay).sign(spendTx, 0)
|
|
36
|
+
return new Spend({
|
|
37
|
+
sourceTXID: sourceTx.id('hex'),
|
|
38
|
+
sourceOutputIndex: 0,
|
|
39
|
+
sourceSatoshis: 1,
|
|
40
|
+
lockingScript,
|
|
41
|
+
transactionVersion: 1,
|
|
42
|
+
otherInputs: [],
|
|
43
|
+
outputs: spendTx.outputs,
|
|
44
|
+
inputIndex: 0,
|
|
45
|
+
unlockingScript,
|
|
46
|
+
inputSequence: 0xffffffff,
|
|
47
|
+
lockTime: 0
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('RPuzzle – additional coverage', () => {
|
|
52
|
+
const k = new BigNumber(12345678)
|
|
53
|
+
const privateKey = new PrivateKey(1)
|
|
54
|
+
const r = getRValue(k)
|
|
55
|
+
|
|
56
|
+
describe('signOutputs variations', () => {
|
|
57
|
+
it('signs with signOutputs=none', async () => {
|
|
58
|
+
const puz = new RPuzzle()
|
|
59
|
+
const spend = await buildRPuzzleSpend(puz, k, privateKey, r, 'none')
|
|
60
|
+
expect(spend.validate()).toBe(true)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('signs with signOutputs=single', async () => {
|
|
64
|
+
const puz = new RPuzzle()
|
|
65
|
+
const spend = await buildRPuzzleSpend(puz, k, privateKey, r, 'single')
|
|
66
|
+
expect(spend.validate()).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('signs with anyoneCanPay=true', async () => {
|
|
70
|
+
const puz = new RPuzzle()
|
|
71
|
+
const spend = await buildRPuzzleSpend(puz, k, privateKey, r, 'all', true)
|
|
72
|
+
expect(spend.validate()).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('estimateLength', () => {
|
|
77
|
+
it('returns 108', async () => {
|
|
78
|
+
const puz = new RPuzzle()
|
|
79
|
+
const result = await puz.unlock(k, privateKey).estimateLength()
|
|
80
|
+
expect(result).toBe(108)
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('missing source transaction', () => {
|
|
85
|
+
it('throws when input has no sourceTransaction', async () => {
|
|
86
|
+
const puz = new RPuzzle()
|
|
87
|
+
const lockingScript = puz.lock(r)
|
|
88
|
+
// Construct a tx where the input has no sourceTransaction
|
|
89
|
+
const spendTx = new Transaction(
|
|
90
|
+
1,
|
|
91
|
+
[{ sourceTXID: ZERO_TXID, sourceOutputIndex: 0, sequence: 0xffffffff }],
|
|
92
|
+
[],
|
|
93
|
+
0
|
|
94
|
+
)
|
|
95
|
+
await expect(
|
|
96
|
+
puz.unlock(k, privateKey).sign(spendTx, 0)
|
|
97
|
+
).rejects.toThrow('The source transaction is needed')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
describe('hash type variants', () => {
|
|
102
|
+
it('SHA256 RPuzzle round-trips using hashed r value', async () => {
|
|
103
|
+
const { sha256 } = await import('../../../primitives/Hash')
|
|
104
|
+
const hashedR = sha256(r)
|
|
105
|
+
const puz = new RPuzzle('SHA256')
|
|
106
|
+
const spend = await buildRPuzzleSpend(puz, k, privateKey, hashedR)
|
|
107
|
+
expect(spend.validate()).toBe(true)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('SHA1 RPuzzle round-trips using hashed r value', async () => {
|
|
111
|
+
const { sha1 } = await import('../../../primitives/Hash')
|
|
112
|
+
const hashedR = sha1(r)
|
|
113
|
+
const puz = new RPuzzle('SHA1')
|
|
114
|
+
const spend = await buildRPuzzleSpend(puz, k, privateKey, hashedR)
|
|
115
|
+
expect(spend.validate()).toBe(true)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('RIPEMD160 RPuzzle round-trips using hashed r value', async () => {
|
|
119
|
+
const { ripemd160 } = await import('../../../primitives/Hash')
|
|
120
|
+
const hashedR = ripemd160(r)
|
|
121
|
+
const puz = new RPuzzle('RIPEMD160')
|
|
122
|
+
const spend = await buildRPuzzleSpend(puz, k, privateKey, hashedR)
|
|
123
|
+
expect(spend.validate()).toBe(true)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('HASH160 RPuzzle round-trips using hashed r value', async () => {
|
|
127
|
+
const { hash160 } = await import('../../../primitives/Hash')
|
|
128
|
+
const hashedR = hash160(r)
|
|
129
|
+
const puz = new RPuzzle('HASH160')
|
|
130
|
+
const spend = await buildRPuzzleSpend(puz, k, privateKey, hashedR)
|
|
131
|
+
expect(spend.validate()).toBe(true)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
})
|
|
@@ -482,4 +482,159 @@ export default class MerklePath {
|
|
|
482
482
|
dropOffsetsFromLevel(dropOffsets, h)
|
|
483
483
|
}
|
|
484
484
|
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Cached leaf finder for extract(). Uses Map-based indexes for O(1) lookups
|
|
488
|
+
* and caches computed intermediate hashes to avoid redundant work.
|
|
489
|
+
*/
|
|
490
|
+
private cachedFindLeaf (
|
|
491
|
+
height: number,
|
|
492
|
+
offset: number,
|
|
493
|
+
sourceIndex: Array<Map<number, MerklePathLeaf>>,
|
|
494
|
+
hashCache: Map<string, MerklePathLeaf | undefined>,
|
|
495
|
+
maxOffset: number
|
|
496
|
+
): MerklePathLeaf | undefined {
|
|
497
|
+
const key = `${height}:${offset}`
|
|
498
|
+
if (hashCache.has(key)) return hashCache.get(key)
|
|
499
|
+
|
|
500
|
+
const doHash = (m: string): string =>
|
|
501
|
+
toHex(hash256(toArray(m, 'hex').reverse()).reverse())
|
|
502
|
+
|
|
503
|
+
let leaf: MerklePathLeaf | undefined = height < sourceIndex.length
|
|
504
|
+
? sourceIndex[height].get(offset)
|
|
505
|
+
: undefined
|
|
506
|
+
|
|
507
|
+
if (leaf != null) {
|
|
508
|
+
hashCache.set(key, leaf)
|
|
509
|
+
return leaf
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (height === 0) {
|
|
513
|
+
hashCache.set(key, undefined)
|
|
514
|
+
return undefined
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const h = height - 1
|
|
518
|
+
const l = offset << 1
|
|
519
|
+
const leaf0 = this.cachedFindLeaf(h, l, sourceIndex, hashCache, maxOffset)
|
|
520
|
+
if (leaf0?.hash == null || leaf0.hash === '') {
|
|
521
|
+
hashCache.set(key, undefined)
|
|
522
|
+
return undefined
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const leaf1 = this.cachedFindLeaf(h, l + 1, sourceIndex, hashCache, maxOffset)
|
|
526
|
+
if (leaf1?.hash == null) {
|
|
527
|
+
if (leaf1?.duplicate === true || (this.path.length === 1 && l === (maxOffset >> h))) {
|
|
528
|
+
leaf = { offset, hash: doHash(leaf0.hash + leaf0.hash) }
|
|
529
|
+
hashCache.set(key, leaf)
|
|
530
|
+
return leaf
|
|
531
|
+
}
|
|
532
|
+
hashCache.set(key, undefined)
|
|
533
|
+
return undefined
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const workinghash = leaf1.duplicate === true
|
|
537
|
+
? doHash(leaf0.hash + leaf0.hash)
|
|
538
|
+
: doHash((leaf1.hash ?? '') + (leaf0.hash ?? ''))
|
|
539
|
+
leaf = { offset, hash: workinghash }
|
|
540
|
+
hashCache.set(key, leaf)
|
|
541
|
+
return leaf
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Extracts a minimal compound MerklePath covering only the specified transaction IDs.
|
|
546
|
+
*
|
|
547
|
+
* Given a compound MerklePath (e.g. all block txids at level 0, or a trimmed
|
|
548
|
+
* compound path), this method reconstructs the sibling hashes at each tree level
|
|
549
|
+
* for every requested txid using cached Map-indexed lookups, then assembles them
|
|
550
|
+
* into a single trimmed compound path.
|
|
551
|
+
*
|
|
552
|
+
* The extracted path is verified to compute the same Merkle root as the source.
|
|
553
|
+
*
|
|
554
|
+
* @param {string[]} txids - Transaction IDs to extract proofs for.
|
|
555
|
+
* @returns {MerklePath} - A new trimmed compound MerklePath covering only the requested txids.
|
|
556
|
+
* @throws {Error} - If no txids are provided, a txid is not found, or the roots do not match.
|
|
557
|
+
*
|
|
558
|
+
* @example
|
|
559
|
+
* // Full block compound path (all txids at level 0)
|
|
560
|
+
* const fullBlock = new MerklePath(height, [allTxidsAtLevel0])
|
|
561
|
+
* // Extract a smaller compound proof covering just two transactions
|
|
562
|
+
* const twoTxProof = fullBlock.extract([txid1, txid2])
|
|
563
|
+
* twoTxProof.computeRoot(txid1) // === fullBlock.computeRoot()
|
|
564
|
+
*/
|
|
565
|
+
extract (txids: string[]): MerklePath {
|
|
566
|
+
if (txids.length === 0) {
|
|
567
|
+
throw new Error('At least one txid must be provided to extract')
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const originalRoot = this.computeRoot()
|
|
571
|
+
const maxOffset = this.path[0].reduce((max, l) => Math.max(max, l.offset), 0)
|
|
572
|
+
const treeHeight = Math.max(this.path.length, 32 - Math.clz32(maxOffset))
|
|
573
|
+
|
|
574
|
+
// Build O(1) lookup indexes for the source path
|
|
575
|
+
const sourceIndex: Array<Map<number, MerklePathLeaf>> = new Array(this.path.length)
|
|
576
|
+
for (let h = 0; h < this.path.length; h++) {
|
|
577
|
+
const map = new Map<number, MerklePathLeaf>()
|
|
578
|
+
for (const leaf of this.path[h]) map.set(leaf.offset, leaf)
|
|
579
|
+
sourceIndex[h] = map
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const hashCache = new Map<string, MerklePathLeaf | undefined>()
|
|
583
|
+
|
|
584
|
+
// Build txid-to-offset index for O(1) lookup
|
|
585
|
+
const txidToOffset = new Map<string, number>()
|
|
586
|
+
for (const leaf of this.path[0]) {
|
|
587
|
+
if (leaf.hash != null) txidToOffset.set(leaf.hash, leaf.offset)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Collect all needed leaves per level
|
|
591
|
+
const neededPerLevel: Array<Map<number, MerklePathLeaf>> = new Array(treeHeight)
|
|
592
|
+
for (let h = 0; h < treeHeight; h++) neededPerLevel[h] = new Map()
|
|
593
|
+
|
|
594
|
+
for (const txid of txids) {
|
|
595
|
+
const txOffset = txidToOffset.get(txid)
|
|
596
|
+
if (txOffset === undefined) {
|
|
597
|
+
throw new Error(`Transaction ID ${txid} not found in the Merkle Path`)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Level 0: the txid leaf + its sibling
|
|
601
|
+
neededPerLevel[0].set(txOffset, { offset: txOffset, txid: true, hash: txid })
|
|
602
|
+
const sib0Offset = txOffset ^ 1
|
|
603
|
+
if (!neededPerLevel[0].has(sib0Offset)) {
|
|
604
|
+
const sib = this.cachedFindLeaf(0, sib0Offset, sourceIndex, hashCache, maxOffset)
|
|
605
|
+
if (sib != null) neededPerLevel[0].set(sib0Offset, sib)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Higher levels: just the sibling at each height
|
|
609
|
+
for (let h = 1; h < treeHeight; h++) {
|
|
610
|
+
const sibOffset = (txOffset >> h) ^ 1
|
|
611
|
+
if (neededPerLevel[h].has(sibOffset)) continue
|
|
612
|
+
const sib = this.cachedFindLeaf(h, sibOffset, sourceIndex, hashCache, maxOffset)
|
|
613
|
+
if (sib != null) {
|
|
614
|
+
neededPerLevel[h].set(sibOffset, sib)
|
|
615
|
+
} else if ((txOffset >> h) === (maxOffset >> h)) {
|
|
616
|
+
neededPerLevel[h].set(sibOffset, { offset: sibOffset, duplicate: true })
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Build sorted compound path
|
|
622
|
+
const compoundPath: Array<Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }>> = new Array(treeHeight)
|
|
623
|
+
for (let h = 0; h < treeHeight; h++) {
|
|
624
|
+
compoundPath[h] = Array.from(neededPerLevel[h].values())
|
|
625
|
+
.sort((a, b) => a.offset - b.offset)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const compound = new MerklePath(this.blockHeight, compoundPath)
|
|
629
|
+
compound.trim()
|
|
630
|
+
|
|
631
|
+
const extractedRoot = compound.computeRoot()
|
|
632
|
+
if (extractedRoot !== originalRoot) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
`Extracted path root ${extractedRoot} does not match original root ${originalRoot}`
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
return compound
|
|
639
|
+
}
|
|
485
640
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import BeefParty from '../BeefParty'
|
|
2
|
+
import { Beef } from '../Beef'
|
|
3
|
+
|
|
4
|
+
describe('BeefParty – additional coverage', () => {
|
|
5
|
+
describe('mergeBeefFromParty', () => {
|
|
6
|
+
it('merges a Beef object directly (non-array branch)', () => {
|
|
7
|
+
const bp = new BeefParty(['alice'])
|
|
8
|
+
const b = new Beef()
|
|
9
|
+
bp.mergeBeefFromParty('alice', b)
|
|
10
|
+
// No error thrown means the Beef object branch executed
|
|
11
|
+
expect(bp.isParty('alice')).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('merges a binary Beef (array branch) via Beef.fromBinary', () => {
|
|
15
|
+
const bp = new BeefParty(['bob'])
|
|
16
|
+
const emptyBeef = new Beef()
|
|
17
|
+
const binary = emptyBeef.toBinary()
|
|
18
|
+
bp.mergeBeefFromParty('bob', binary)
|
|
19
|
+
expect(bp.isParty('bob')).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BroadcastResponse,
|
|
3
|
+
BroadcastFailure,
|
|
4
|
+
isBroadcastResponse,
|
|
5
|
+
isBroadcastFailure
|
|
6
|
+
} from '../../transaction/Broadcaster'
|
|
7
|
+
|
|
8
|
+
// Broadcaster.ts contains two interfaces (BroadcastResponse, BroadcastFailure, Broadcaster)
|
|
9
|
+
// and two exported type-guard functions. The interfaces have no runtime representation,
|
|
10
|
+
// so coverage comes entirely from exercising isBroadcastResponse and isBroadcastFailure.
|
|
11
|
+
|
|
12
|
+
describe('isBroadcastResponse', () => {
|
|
13
|
+
it('returns true for an object with status "success"', () => {
|
|
14
|
+
const r: BroadcastResponse = {
|
|
15
|
+
status: 'success',
|
|
16
|
+
txid: 'abc123',
|
|
17
|
+
message: 'broadcast successful'
|
|
18
|
+
}
|
|
19
|
+
expect(isBroadcastResponse(r)).toBe(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('returns false for an object with status "error"', () => {
|
|
23
|
+
const r: BroadcastFailure = {
|
|
24
|
+
status: 'error',
|
|
25
|
+
code: '500',
|
|
26
|
+
description: 'Internal Server Error'
|
|
27
|
+
}
|
|
28
|
+
expect(isBroadcastResponse(r)).toBe(false)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('narrows the type to BroadcastResponse inside a conditional', () => {
|
|
32
|
+
const r: BroadcastResponse | BroadcastFailure = {
|
|
33
|
+
status: 'success',
|
|
34
|
+
txid: 'deadbeef',
|
|
35
|
+
message: 'ok',
|
|
36
|
+
competingTxs: ['aabbcc']
|
|
37
|
+
}
|
|
38
|
+
if (isBroadcastResponse(r)) {
|
|
39
|
+
expect(r.txid).toBe('deadbeef')
|
|
40
|
+
expect(r.message).toBe('ok')
|
|
41
|
+
expect(r.competingTxs).toEqual(['aabbcc'])
|
|
42
|
+
} else {
|
|
43
|
+
fail('Expected isBroadcastResponse to return true')
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns false even when the failure object has extra fields', () => {
|
|
48
|
+
const r: BroadcastFailure = {
|
|
49
|
+
status: 'error',
|
|
50
|
+
code: '404',
|
|
51
|
+
txid: 'txidfail',
|
|
52
|
+
description: 'Not found',
|
|
53
|
+
more: { detail: 'extra' }
|
|
54
|
+
}
|
|
55
|
+
expect(isBroadcastResponse(r)).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('isBroadcastFailure', () => {
|
|
60
|
+
it('returns true for an object with status "error"', () => {
|
|
61
|
+
const r: BroadcastFailure = {
|
|
62
|
+
status: 'error',
|
|
63
|
+
code: 'ERR_UNKNOWN',
|
|
64
|
+
description: 'Something went wrong'
|
|
65
|
+
}
|
|
66
|
+
expect(isBroadcastFailure(r)).toBe(true)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('returns false for an object with status "success"', () => {
|
|
70
|
+
const r: BroadcastResponse = {
|
|
71
|
+
status: 'success',
|
|
72
|
+
txid: 'txid1',
|
|
73
|
+
message: 'done'
|
|
74
|
+
}
|
|
75
|
+
expect(isBroadcastFailure(r)).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('narrows the type to BroadcastFailure inside a conditional', () => {
|
|
79
|
+
const r: BroadcastResponse | BroadcastFailure = {
|
|
80
|
+
status: 'error',
|
|
81
|
+
code: '503',
|
|
82
|
+
txid: 'tx503',
|
|
83
|
+
description: 'Service unavailable',
|
|
84
|
+
more: { raw: 'body' }
|
|
85
|
+
}
|
|
86
|
+
if (isBroadcastFailure(r)) {
|
|
87
|
+
expect(r.code).toBe('503')
|
|
88
|
+
expect(r.description).toBe('Service unavailable')
|
|
89
|
+
expect(r.txid).toBe('tx503')
|
|
90
|
+
expect(r.more).toEqual({ raw: 'body' })
|
|
91
|
+
} else {
|
|
92
|
+
fail('Expected isBroadcastFailure to return true')
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('returns false even when the success object has an optional competingTxs field', () => {
|
|
97
|
+
const r: BroadcastResponse = {
|
|
98
|
+
status: 'success',
|
|
99
|
+
txid: 't1',
|
|
100
|
+
message: 'm1',
|
|
101
|
+
competingTxs: ['other']
|
|
102
|
+
}
|
|
103
|
+
expect(isBroadcastFailure(r)).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('handles BroadcastFailure with only the required fields', () => {
|
|
107
|
+
const r: BroadcastFailure = {
|
|
108
|
+
status: 'error',
|
|
109
|
+
code: '400',
|
|
110
|
+
description: 'Bad request'
|
|
111
|
+
}
|
|
112
|
+
expect(isBroadcastFailure(r)).toBe(true)
|
|
113
|
+
if (isBroadcastFailure(r)) {
|
|
114
|
+
expect(r.txid).toBeUndefined()
|
|
115
|
+
expect(r.more).toBeUndefined()
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('BroadcastResponse interface shape', () => {
|
|
121
|
+
it('accepts competingTxs as an optional field', () => {
|
|
122
|
+
const withCompeting: BroadcastResponse = {
|
|
123
|
+
status: 'success',
|
|
124
|
+
txid: 'tx',
|
|
125
|
+
message: 'msg',
|
|
126
|
+
competingTxs: ['tx1', 'tx2']
|
|
127
|
+
}
|
|
128
|
+
expect(withCompeting.competingTxs).toHaveLength(2)
|
|
129
|
+
|
|
130
|
+
const withoutCompeting: BroadcastResponse = {
|
|
131
|
+
status: 'success',
|
|
132
|
+
txid: 'tx',
|
|
133
|
+
message: 'msg'
|
|
134
|
+
}
|
|
135
|
+
expect(withoutCompeting.competingTxs).toBeUndefined()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('BroadcastFailure interface shape', () => {
|
|
140
|
+
it('accepts txid and more as optional fields', () => {
|
|
141
|
+
const full: BroadcastFailure = {
|
|
142
|
+
status: 'error',
|
|
143
|
+
code: '422',
|
|
144
|
+
txid: 'txfull',
|
|
145
|
+
description: 'Unprocessable',
|
|
146
|
+
more: { hints: ['check input'] }
|
|
147
|
+
}
|
|
148
|
+
expect(full.txid).toBe('txfull')
|
|
149
|
+
expect(full.more).toEqual({ hints: ['check input'] })
|
|
150
|
+
|
|
151
|
+
const minimal: BroadcastFailure = {
|
|
152
|
+
status: 'error',
|
|
153
|
+
code: '422',
|
|
154
|
+
description: 'Unprocessable'
|
|
155
|
+
}
|
|
156
|
+
expect(minimal.txid).toBeUndefined()
|
|
157
|
+
expect(minimal.more).toBeUndefined()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import MerklePath from '../../transaction/MerklePath'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate a random 32-byte hex hash.
|
|
5
|
+
*/
|
|
6
|
+
function randomHash (): string {
|
|
7
|
+
const bytes = new Uint8Array(32)
|
|
8
|
+
for (let i = 0; i < 32; i++) bytes[i] = Math.floor(Math.random() * 256)
|
|
9
|
+
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a full-block compound MerklePath with `count` transactions at level 0.
|
|
14
|
+
* All leaves are txid: true; if count is odd the last leaf gets duplicate: true.
|
|
15
|
+
*/
|
|
16
|
+
function buildFullBlockPath (count: number): { mp: MerklePath, txids: string[] } {
|
|
17
|
+
const txids: string[] = []
|
|
18
|
+
const leaves: Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }> = []
|
|
19
|
+
for (let i = 0; i < count; i++) {
|
|
20
|
+
const h = randomHash()
|
|
21
|
+
txids.push(h)
|
|
22
|
+
leaves.push({ offset: i, hash: h, txid: true })
|
|
23
|
+
}
|
|
24
|
+
if (count % 2 === 1) {
|
|
25
|
+
leaves.push({ offset: count, duplicate: true })
|
|
26
|
+
}
|
|
27
|
+
const mp = new MerklePath(1, [leaves])
|
|
28
|
+
return { mp, txids }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pick `n` random items from `arr` without replacement.
|
|
33
|
+
*/
|
|
34
|
+
function pickRandom<T> (arr: T[], n: number): T[] {
|
|
35
|
+
const copy = [...arr]
|
|
36
|
+
const result: T[] = []
|
|
37
|
+
for (let i = 0; i < n && copy.length > 0; i++) {
|
|
38
|
+
const idx = Math.floor(Math.random() * copy.length)
|
|
39
|
+
result.push(copy.splice(idx, 1)[0])
|
|
40
|
+
}
|
|
41
|
+
return result
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('MerklePath.extract() benchmarks', () => {
|
|
45
|
+
// Pre-build paths once so construction time is excluded from extract timing.
|
|
46
|
+
let path101: { mp: MerklePath, txids: string[] }
|
|
47
|
+
let path501: { mp: MerklePath, txids: string[] }
|
|
48
|
+
let path999: { mp: MerklePath, txids: string[] }
|
|
49
|
+
|
|
50
|
+
beforeAll(() => {
|
|
51
|
+
path101 = buildFullBlockPath(101)
|
|
52
|
+
path501 = buildFullBlockPath(501)
|
|
53
|
+
path999 = buildFullBlockPath(999)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const runBench = (
|
|
57
|
+
label: string,
|
|
58
|
+
getPath: () => { mp: MerklePath, txids: string[] },
|
|
59
|
+
extractCount: number,
|
|
60
|
+
iterations: number = 5
|
|
61
|
+
): void => {
|
|
62
|
+
it(label, () => {
|
|
63
|
+
const { mp, txids } = getPath()
|
|
64
|
+
const targets = pickRandom(txids, extractCount)
|
|
65
|
+
|
|
66
|
+
const times: number[] = []
|
|
67
|
+
for (let i = 0; i < iterations; i++) {
|
|
68
|
+
const start = performance.now()
|
|
69
|
+
const extracted = mp.extract(targets)
|
|
70
|
+
const elapsed = performance.now() - start
|
|
71
|
+
times.push(elapsed)
|
|
72
|
+
// Correctness check on every iteration
|
|
73
|
+
for (const txid of targets) {
|
|
74
|
+
expect(extracted.computeRoot(txid)).toBe(mp.computeRoot(txid))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const avg = times.reduce((a, b) => a + b, 0) / times.length
|
|
79
|
+
const min = Math.min(...times)
|
|
80
|
+
const max = Math.max(...times)
|
|
81
|
+
console.log(
|
|
82
|
+
`[${label}] avg=${avg.toFixed(1)}ms min=${min.toFixed(1)}ms max=${max.toFixed(1)}ms (${iterations} runs)`
|
|
83
|
+
)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- 101 txids ---
|
|
88
|
+
runBench('101 txids, extract 1', () => path101, 1)
|
|
89
|
+
runBench('101 txids, extract 5', () => path101, 5)
|
|
90
|
+
runBench('101 txids, extract 10', () => path101, 10)
|
|
91
|
+
runBench('101 txids, extract 50', () => path101, 50)
|
|
92
|
+
|
|
93
|
+
// --- 501 txids ---
|
|
94
|
+
runBench('501 txids, extract 1', () => path501, 1)
|
|
95
|
+
runBench('501 txids, extract 5', () => path501, 5)
|
|
96
|
+
runBench('501 txids, extract 10', () => path501, 10)
|
|
97
|
+
runBench('501 txids, extract 50', () => path501, 50)
|
|
98
|
+
|
|
99
|
+
// --- 999 txids ---
|
|
100
|
+
runBench('999 txids, extract 1', () => path999, 1)
|
|
101
|
+
runBench('999 txids, extract 5', () => path999, 5)
|
|
102
|
+
runBench('999 txids, extract 10', () => path999, 10)
|
|
103
|
+
runBench('999 txids, extract 50', () => path999, 50)
|
|
104
|
+
runBench('999 txids, extract 100', () => path999, 100, 3)
|
|
105
|
+
})
|
|
@@ -391,6 +391,86 @@ describe('MerklePath', () => {
|
|
|
391
391
|
expect(splitTx5.computeRoot(tx[5])).toBe(merkleroot)
|
|
392
392
|
expect(splitTx8.computeRoot(tx[8])).toBe(merkleroot)
|
|
393
393
|
})
|
|
394
|
+
describe('extract()', () => {
|
|
395
|
+
it('extracts a single-txid proof from a full block compound path', () => {
|
|
396
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
397
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
398
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
399
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
400
|
+
|
|
401
|
+
const extracted = fullBlock.extract([tx[2]])
|
|
402
|
+
expect(extracted.computeRoot(tx[2])).toBe(merkleroot)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('extracts a multi-txid compound proof from a full block compound path', () => {
|
|
406
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
407
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
408
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
409
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
410
|
+
|
|
411
|
+
const extracted = fullBlock.extract([tx[2], tx[5], tx[8]])
|
|
412
|
+
expect(extracted.computeRoot(tx[2])).toBe(merkleroot)
|
|
413
|
+
expect(extracted.computeRoot(tx[5])).toBe(merkleroot)
|
|
414
|
+
expect(extracted.computeRoot(tx[8])).toBe(merkleroot)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('extracted path serializes and deserializes correctly', () => {
|
|
418
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
419
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
420
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
421
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
422
|
+
|
|
423
|
+
const extracted = fullBlock.extract([tx[2], tx[8]])
|
|
424
|
+
const roundTripped = MerklePath.fromHex(extracted.toHex())
|
|
425
|
+
expect(roundTripped.computeRoot(tx[2])).toBe(merkleroot)
|
|
426
|
+
expect(roundTripped.computeRoot(tx[8])).toBe(merkleroot)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it('extracted path is smaller than the full block path', () => {
|
|
430
|
+
const { height, tx } = BLOCK_125632
|
|
431
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
432
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
433
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
434
|
+
|
|
435
|
+
const extracted = fullBlock.extract([tx[2], tx[5]])
|
|
436
|
+
expect(extracted.toBinary().length).toBeLessThan(fullBlock.toBinary().length)
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('extract from a trimmed multi-level compound path also works', () => {
|
|
440
|
+
const { height, merkleroot, tx } = BLOCK_125632
|
|
441
|
+
const [pathA, pathB] = buildSplitPaths()
|
|
442
|
+
pathA.combine(pathB)
|
|
443
|
+
// pathA is now a trimmed multi-level compound path for tx[2] and tx[3]
|
|
444
|
+
const txid2 = BRC74TXID2
|
|
445
|
+
const compound = new MerklePath(BRC74JSON.blockHeight, BRC74JSON.path)
|
|
446
|
+
const extracted = compound.extract([txid2])
|
|
447
|
+
expect(extracted.computeRoot(txid2)).toBe(compound.computeRoot(txid2))
|
|
448
|
+
// BLOCK_125632 variant
|
|
449
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
450
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
451
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
452
|
+
const ex = fullBlock.extract([tx[0], tx[10]])
|
|
453
|
+
expect(ex.computeRoot(tx[0])).toBe(merkleroot)
|
|
454
|
+
expect(ex.computeRoot(tx[10])).toBe(merkleroot)
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('throws when no txids are provided', () => {
|
|
458
|
+
const { height, tx } = BLOCK_125632
|
|
459
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
460
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
461
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
462
|
+
expect(() => fullBlock.extract([])).toThrow('At least one txid must be provided')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('throws when a txid is not in the path', () => {
|
|
466
|
+
const { height, tx } = BLOCK_125632
|
|
467
|
+
const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
|
|
468
|
+
if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
|
|
469
|
+
const fullBlock = new MerklePath(height, [leafs])
|
|
470
|
+
expect(() => fullBlock.extract(['deadbeef'.repeat(8)])).toThrow()
|
|
471
|
+
})
|
|
472
|
+
})
|
|
473
|
+
|
|
394
474
|
it('findOrComputeLeaf duplicates leaf0 when leaf1 carries both a hash and duplicate=true', () => {
|
|
395
475
|
// Covers the leaf1.duplicate === true branch inside findOrComputeLeaf.
|
|
396
476
|
// That branch is reached when leaf1.hash is non-null (bypassing the null-check above it)
|