@bsv/sdk 2.0.11 → 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/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 +168 -27
- 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/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 +168 -27
- 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/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 +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 +3 -3
- 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/auth/utils/__tests/validateCertificates.test.ts +12 -9
- 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/LocalKVStore.test.ts +4 -6
- package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
- package/src/overlay-tools/HostReputationTracker.ts +17 -14
- 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/PrivateKey.ts +3 -3
- 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/Spend.ts +19 -11
- 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/storage/StorageDownloader.ts +6 -6
- package/src/storage/StorageUtils.ts +1 -1
- package/src/transaction/MerklePath.ts +196 -36
- 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 +232 -21
- 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
|
+
})
|
|
@@ -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())
|
|
@@ -291,11 +286,16 @@ export default class MerklePath {
|
|
|
291
286
|
const offset = (index >> height) ^ 1
|
|
292
287
|
const leaf = this.findOrComputeLeaf(height, offset)
|
|
293
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
|
+
}
|
|
294
295
|
throw new Error(`Missing hash for index ${index} at height ${height}`)
|
|
295
|
-
}
|
|
296
|
-
if (leaf.duplicate === true) {
|
|
296
|
+
} else if (leaf.duplicate === true) {
|
|
297
297
|
workingHash = hash((workingHash ?? '') + (workingHash ?? ''))
|
|
298
|
-
} else if (offset % 2
|
|
298
|
+
} else if (offset % 2 === 1) {
|
|
299
299
|
workingHash = hash((leaf.hash ?? '') + (workingHash ?? ''))
|
|
300
300
|
} else {
|
|
301
301
|
workingHash = hash((workingHash ?? '') + (leaf.hash ?? ''))
|
|
@@ -334,7 +334,20 @@ export default class MerklePath {
|
|
|
334
334
|
if (leaf0 == null || leaf0.hash == null || leaf0.hash === '') return undefined
|
|
335
335
|
|
|
336
336
|
const leaf1 = this.findOrComputeLeaf(h, l + 1)
|
|
337
|
-
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
|
+
}
|
|
338
351
|
|
|
339
352
|
let workinghash: string
|
|
340
353
|
if (leaf1.duplicate === true) {
|
|
@@ -392,26 +405,18 @@ export default class MerklePath {
|
|
|
392
405
|
const combinedPath: Array<Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }>> = []
|
|
393
406
|
for (let h = 0; h < this.path.length; h++) {
|
|
394
407
|
combinedPath.push([])
|
|
395
|
-
for (
|
|
396
|
-
combinedPath[h].push(
|
|
408
|
+
for (const leaf of this.path[h]) {
|
|
409
|
+
combinedPath[h].push(leaf)
|
|
397
410
|
}
|
|
398
|
-
for (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
} 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) {
|
|
406
418
|
// Ensure that any elements which appear in both are not downgraded to a non txid.
|
|
407
|
-
|
|
408
|
-
const target = combinedPath[h].find(
|
|
409
|
-
(leaf) => leaf.offset === other.path[h][l].offset
|
|
410
|
-
)
|
|
411
|
-
if (target !== null && target !== undefined) {
|
|
412
|
-
target.txid = true
|
|
413
|
-
}
|
|
414
|
-
}
|
|
419
|
+
existingLeaf.txid = true
|
|
415
420
|
}
|
|
416
421
|
}
|
|
417
422
|
}
|
|
@@ -477,4 +482,159 @@ export default class MerklePath {
|
|
|
477
482
|
dropOffsetsFromLevel(dropOffsets, h)
|
|
478
483
|
}
|
|
479
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
|
+
}
|
|
480
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
|
+
})
|