@bsv/sdk 2.1.4 → 2.1.6
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 +7 -7
- package/dist/cjs/src/compat/Utxo.js +1 -1
- package/dist/cjs/src/compat/Utxo.js.map +1 -1
- package/dist/cjs/src/kvstore/GlobalKVStore.js +18 -3
- package/dist/cjs/src/kvstore/GlobalKVStore.js.map +1 -1
- package/dist/cjs/src/kvstore/types.js.map +1 -1
- package/dist/cjs/src/primitives/Hash.js +4 -2
- package/dist/cjs/src/primitives/Hash.js.map +1 -1
- package/dist/cjs/src/script/Spend.js +8 -1
- package/dist/cjs/src/script/Spend.js.map +1 -1
- package/dist/cjs/src/transaction/BdkVerifierInterface.js +3 -0
- package/dist/cjs/src/transaction/BdkVerifierInterface.js.map +1 -0
- package/dist/cjs/src/transaction/Transaction.js +35 -16
- package/dist/cjs/src/transaction/Transaction.js.map +1 -1
- package/dist/cjs/src/transaction/index.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/compat/Utxo.js +1 -1
- package/dist/esm/src/compat/Utxo.js.map +1 -1
- package/dist/esm/src/kvstore/GlobalKVStore.js +18 -3
- package/dist/esm/src/kvstore/GlobalKVStore.js.map +1 -1
- package/dist/esm/src/kvstore/types.js.map +1 -1
- package/dist/esm/src/primitives/Hash.js +4 -2
- package/dist/esm/src/primitives/Hash.js.map +1 -1
- package/dist/esm/src/script/Spend.js +8 -1
- package/dist/esm/src/script/Spend.js.map +1 -1
- package/dist/esm/src/transaction/BdkVerifierInterface.js +2 -0
- package/dist/esm/src/transaction/BdkVerifierInterface.js.map +1 -0
- package/dist/esm/src/transaction/Transaction.js +35 -16
- package/dist/esm/src/transaction/Transaction.js.map +1 -1
- package/dist/esm/src/transaction/index.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/kvstore/GlobalKVStore.d.ts.map +1 -1
- package/dist/types/src/kvstore/types.d.ts +27 -0
- package/dist/types/src/kvstore/types.d.ts.map +1 -1
- package/dist/types/src/primitives/Hash.d.ts.map +1 -1
- package/dist/types/src/script/Spend.d.ts.map +1 -1
- package/dist/types/src/transaction/BdkVerifierInterface.d.ts +22 -0
- package/dist/types/src/transaction/BdkVerifierInterface.d.ts.map +1 -0
- package/dist/types/src/transaction/Transaction.d.ts +2 -1
- package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
- package/dist/types/src/transaction/index.d.ts +1 -0
- package/dist/types/src/transaction/index.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/package.json +7 -7
- package/src/compat/Utxo.ts +1 -1
- package/src/compat/__tests/Mnemonic.additional.test.ts +1 -1
- package/src/compat/__tests/Mnemonic.test.ts +1 -1
- package/src/kvstore/GlobalKVStore.ts +18 -3
- package/src/kvstore/__tests/GlobalKVStore.test.ts +36 -0
- package/src/kvstore/types.ts +28 -0
- package/src/overlay-tools/__tests/Historian.test.ts +1 -1
- package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +1 -1
- package/src/primitives/Hash.ts +4 -2
- package/src/primitives/__tests/BigNumber.constructor.test.ts +1 -1
- package/src/primitives/__tests/DRBG.test.ts +4 -12
- package/src/primitives/__tests/Point.additional.test.ts +2 -2
- package/src/primitives/__tests/Point.test.ts +1 -1
- package/src/primitives/__tests/SymmetricKeyCompatibility.test.ts +4 -4
- package/src/registry/__tests/RegistryClient.additional.test.ts +4 -4
- package/src/registry/__tests/RegistryClient.test.ts +2 -2
- package/src/script/Spend.ts +8 -1
- package/src/script/__tests/Spend.codeseparator.test.ts +88 -0
- package/src/storage/__tests/StorageUploader.test.ts +7 -7
- package/src/transaction/BdkVerifierInterface.ts +22 -0
- package/src/transaction/Transaction.ts +40 -17
- package/src/transaction/__tests/Transaction.test.ts +3 -3
- package/src/transaction/__tests/Transaction.verifier.test.ts +72 -0
- package/src/transaction/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsv/sdk",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "BSV Blockchain Software Development Kit",
|
|
6
6
|
"main": "dist/cjs/mod.js",
|
|
@@ -239,21 +239,21 @@
|
|
|
239
239
|
"devDependencies": {
|
|
240
240
|
"@eslint/js": "^10.0.1",
|
|
241
241
|
"@jest/globals": "^30.4.1",
|
|
242
|
-
"@rspack/cli": "^2.0.
|
|
243
|
-
"@rspack/core": "^2.0.
|
|
242
|
+
"@rspack/cli": "^2.0.8",
|
|
243
|
+
"@rspack/core": "^2.0.8",
|
|
244
244
|
"@types/jest": "^30.0.0",
|
|
245
|
-
"@types/node": "^25.9.
|
|
246
|
-
"eslint": "^10.
|
|
245
|
+
"@types/node": "^25.9.3",
|
|
246
|
+
"eslint": "^10.5.0",
|
|
247
247
|
"globals": "^17.6.0",
|
|
248
248
|
"jest": "^30.4.2",
|
|
249
249
|
"jest-environment-jsdom": "^30.4.1",
|
|
250
250
|
"ts-jest": "^29.4.11",
|
|
251
|
-
"ts-loader": "^9.
|
|
251
|
+
"ts-loader": "^9.6.0",
|
|
252
252
|
"ts-standard": "^12.0.2",
|
|
253
253
|
"ts2md": "^0.2.8",
|
|
254
254
|
"tsconfig-to-dual-package": "^1.2.0",
|
|
255
255
|
"typescript": "^6.0.3",
|
|
256
|
-
"typescript-eslint": "^8.
|
|
256
|
+
"typescript-eslint": "^8.61.0"
|
|
257
257
|
},
|
|
258
258
|
"ts-standard": {
|
|
259
259
|
"project": "tsconfig.eslint.json",
|
package/src/compat/Utxo.ts
CHANGED
|
@@ -44,7 +44,7 @@ export default function fromUtxo (
|
|
|
44
44
|
}
|
|
45
45
|
): TransactionInput {
|
|
46
46
|
const sourceTransaction = new Transaction(0, [], [], 0)
|
|
47
|
-
sourceTransaction.outputs = Array(utxo.vout + 1).fill(null)
|
|
47
|
+
sourceTransaction.outputs = new Array(utxo.vout + 1).fill(null)
|
|
48
48
|
sourceTransaction.outputs[utxo.vout] = {
|
|
49
49
|
satoshis: utxo.satoshis,
|
|
50
50
|
lockingScript: LockingScript.fromHex(utxo.script)
|
|
@@ -15,7 +15,7 @@ describe('Mnemonic – additional coverage', () => {
|
|
|
15
15
|
})
|
|
16
16
|
|
|
17
17
|
it('uses 128 bits when bits is NaN', () => {
|
|
18
|
-
const m = new Mnemonic().fromRandom(NaN)
|
|
18
|
+
const m = new Mnemonic().fromRandom(Number.NaN)
|
|
19
19
|
expect(m.mnemonic.split(' ')).toHaveLength(12)
|
|
20
20
|
})
|
|
21
21
|
|
|
@@ -41,7 +41,7 @@ describe('Mnemonic', function () {
|
|
|
41
41
|
m = new Mnemonic().fromRandom(128)
|
|
42
42
|
expect(m.check()).toEqual(true)
|
|
43
43
|
|
|
44
|
-
const entropy = Array(32)
|
|
44
|
+
const entropy = new Array(32)
|
|
45
45
|
entropy.fill(0)
|
|
46
46
|
m = new Mnemonic().fromEntropy(entropy)
|
|
47
47
|
expect(m.check()).toEqual(true)
|
|
@@ -89,11 +89,26 @@ export class GlobalKVStore {
|
|
|
89
89
|
this.config = { ...DEFAULT_CONFIG, ...config }
|
|
90
90
|
this.wallet = config.wallet ?? new WalletClient()
|
|
91
91
|
this.historian = new Historian<string, KVContext>(kvStoreInterpreter)
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
// Resolve overlay hosts via, in order of precedence: an injected resolver,
|
|
93
|
+
// otherwise a default resolver built from `networkPreset` plus any
|
|
94
|
+
// `hostOverrides` / `slapTrackers`. The same resolver is shared with the
|
|
95
|
+
// topic broadcaster so read lookups and the broadcaster's SHIP host
|
|
96
|
+
// discovery both go through it. Note this shares the *lookup* path, not the
|
|
97
|
+
// broadcast target: writes still go to whatever hosts the `ls_ship` SHIP
|
|
98
|
+
// lookup returns, so pinning the broadcast backend requires that lookup to
|
|
99
|
+
// return the desired host (a host override alone does not force it). With no
|
|
100
|
+
// overrides this is behaviourally identical to the previous
|
|
101
|
+
// networkPreset-only construction.
|
|
102
|
+
// `hostOverrides` / `slapTrackers` are passed straight through; LookupResolver
|
|
103
|
+
// already falls back to its defaults when they're undefined.
|
|
104
|
+
this.lookupResolver = this.config.lookupResolver ?? new LookupResolver({
|
|
105
|
+
networkPreset: this.config.networkPreset,
|
|
106
|
+
hostOverrides: this.config.hostOverrides,
|
|
107
|
+
slapTrackers: this.config.slapTrackers
|
|
94
108
|
})
|
|
95
109
|
this.topicBroadcaster = new TopicBroadcaster(this.config.topics as string[], {
|
|
96
|
-
networkPreset: this.config.networkPreset
|
|
110
|
+
networkPreset: this.config.networkPreset,
|
|
111
|
+
resolver: this.lookupResolver
|
|
97
112
|
})
|
|
98
113
|
}
|
|
99
114
|
|
|
@@ -264,6 +264,42 @@ describe('GlobalKVStore', () => {
|
|
|
264
264
|
it('initializes Historian with kvStoreInterpreter', () => {
|
|
265
265
|
expect(MockHistorian).toHaveBeenCalledWith(kvStoreInterpreter)
|
|
266
266
|
})
|
|
267
|
+
|
|
268
|
+
it('builds a default resolver from networkPreset and shares it with the broadcaster', () => {
|
|
269
|
+
MockLookupResolver.mockClear()
|
|
270
|
+
MockTopicBroadcaster.mockClear()
|
|
271
|
+
const store = new GlobalKVStore({ wallet: mockWallet, networkPreset: 'testnet' })
|
|
272
|
+
expect(store).toBeInstanceOf(GlobalKVStore)
|
|
273
|
+
expect(MockLookupResolver).toHaveBeenCalledWith({ networkPreset: 'testnet' })
|
|
274
|
+
// The broadcaster must reuse the same resolver so read lookups and the
|
|
275
|
+
// broadcaster's SHIP host discovery share one lookup path.
|
|
276
|
+
const broadcasterConfig = MockTopicBroadcaster.mock.calls[0][1] as { resolver?: unknown }
|
|
277
|
+
expect(broadcasterConfig.resolver).toBe(mockResolver)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('passes hostOverrides and slapTrackers into the default resolver', () => {
|
|
281
|
+
MockLookupResolver.mockClear()
|
|
282
|
+
const hostOverrides = { ls_kvstore: ['https://host.example'] }
|
|
283
|
+
const slapTrackers = ['https://slap.example']
|
|
284
|
+
const store = new GlobalKVStore({ wallet: mockWallet, hostOverrides, slapTrackers })
|
|
285
|
+
expect(store).toBeInstanceOf(GlobalKVStore)
|
|
286
|
+
expect(MockLookupResolver).toHaveBeenCalledWith({
|
|
287
|
+
networkPreset: 'mainnet',
|
|
288
|
+
hostOverrides,
|
|
289
|
+
slapTrackers,
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('uses an injected lookupResolver verbatim and does not build a default one', () => {
|
|
294
|
+
MockLookupResolver.mockClear()
|
|
295
|
+
MockTopicBroadcaster.mockClear()
|
|
296
|
+
const injected = { query: jest.fn() } as unknown as InstanceType<typeof LookupResolver>
|
|
297
|
+
const store = new GlobalKVStore({ wallet: mockWallet, lookupResolver: injected })
|
|
298
|
+
expect(store).toBeInstanceOf(GlobalKVStore)
|
|
299
|
+
expect(MockLookupResolver).not.toHaveBeenCalled()
|
|
300
|
+
const broadcasterConfig = MockTopicBroadcaster.mock.calls[0][1] as { resolver?: unknown }
|
|
301
|
+
expect(broadcasterConfig.resolver).toBe(injected)
|
|
302
|
+
})
|
|
267
303
|
})
|
|
268
304
|
|
|
269
305
|
// --------------------------------------------------------------------------
|
package/src/kvstore/types.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Beef } from '../transaction/Beef.js'
|
|
2
2
|
import { PubKeyHex, WalletProtocol } from '../wallet/Wallet.interfaces.js'
|
|
3
3
|
import { WalletInterface } from '../wallet/index.js'
|
|
4
|
+
// Type-only import — erased at runtime, so it introduces no module cycle with overlay-tools.
|
|
5
|
+
import type { LookupResolver } from '../overlay-tools/index.js'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Configuration interface for GlobalKVStore operations.
|
|
@@ -23,6 +25,32 @@ export interface KVStoreConfig {
|
|
|
23
25
|
wallet?: WalletInterface
|
|
24
26
|
/** Network preset for overlay services */
|
|
25
27
|
networkPreset?: 'mainnet' | 'testnet' | 'local'
|
|
28
|
+
/**
|
|
29
|
+
* A pre-built lookup resolver to use for all overlay queries — both reads and
|
|
30
|
+
* write-host (SHIP) discovery. When provided, it takes precedence and
|
|
31
|
+
* `hostOverrides` / `slapTrackers` are ignored for resolver construction.
|
|
32
|
+
* Use this to fully control overlay host resolution.
|
|
33
|
+
*/
|
|
34
|
+
lookupResolver?: LookupResolver
|
|
35
|
+
/**
|
|
36
|
+
* Per-service overlay host overrides (`serviceName -> hosts`), applied when
|
|
37
|
+
* the store builds its default lookup resolver. This pins which hosts answer
|
|
38
|
+
* *lookup* queries for a given service (e.g. read lookups via `ls_kvstore`),
|
|
39
|
+
* instead of discovering them via SLAP.
|
|
40
|
+
*
|
|
41
|
+
* Note this does not by itself pin the *broadcast* target: writes are
|
|
42
|
+
* submitted to the hosts that the `ls_ship` SHIP lookup returns, so an
|
|
43
|
+
* `ls_ship` override only changes which tracker answers — the broadcast host
|
|
44
|
+
* is whatever advertisements that lookup names. To force writes to a specific
|
|
45
|
+
* backend, use a resolver / SHIP setup whose `ls_ship` results return the
|
|
46
|
+
* desired host. Ignored when `lookupResolver` is supplied.
|
|
47
|
+
*/
|
|
48
|
+
hostOverrides?: Record<string, string[]>
|
|
49
|
+
/**
|
|
50
|
+
* Override the SLAP trackers used by the default lookup resolver. Ignored when
|
|
51
|
+
* `lookupResolver` is supplied.
|
|
52
|
+
*/
|
|
53
|
+
slapTrackers?: string[]
|
|
26
54
|
/** Whether to accept delayed broadcast */
|
|
27
55
|
acceptDelayedBroadcast?: boolean
|
|
28
56
|
/** Whether to let overlay handle broadcasting (prevents UTXO spending on rejection) */
|
|
@@ -34,7 +34,7 @@ function makeMockTx(txid: string, outputs: any[] = [], inputs: any[] = []): MTx
|
|
|
34
34
|
|
|
35
35
|
function makeMockOutput(scriptHex?: string): TransactionOutput {
|
|
36
36
|
const hex = scriptHex || '76a914' // Default to P2PKH prefix if no script provided
|
|
37
|
-
const scriptArray = hex.match(/.{2}/g)?.map(byte => parseInt(byte, 16)) || [0x76, 0xa9, 0x14]
|
|
37
|
+
const scriptArray = hex.match(/.{2}/g)?.map(byte => Number.parseInt(byte, 16)) || [0x76, 0xa9, 0x14]
|
|
38
38
|
|
|
39
39
|
return {
|
|
40
40
|
satoshis: 1,
|
|
@@ -93,7 +93,7 @@ describe('HostReputationTracker – additional coverage', () => {
|
|
|
93
93
|
|
|
94
94
|
it('treats NaN latency as DEFAULT_LATENCY_MS (1500)', () => {
|
|
95
95
|
const t = new HostReputationTracker()
|
|
96
|
-
t.recordSuccess('https://host.com', NaN)
|
|
96
|
+
t.recordSuccess('https://host.com', Number.NaN)
|
|
97
97
|
const snap = t.snapshot('https://host.com')!
|
|
98
98
|
expect(snap.avgLatencyMs).toBe(1500)
|
|
99
99
|
})
|
package/src/primitives/Hash.ts
CHANGED
|
@@ -261,8 +261,10 @@ function appendUtf8CodeUnit (msg: string, i: number, out: number[]): number {
|
|
|
261
261
|
|
|
262
262
|
function utf8StringToArray (msg: string): number[] {
|
|
263
263
|
const res: number[] = []
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
let i = 0
|
|
265
|
+
while (i < msg.length) {
|
|
266
|
+
const lastConsumed = appendUtf8CodeUnit(msg, i, res)
|
|
267
|
+
i = lastConsumed + 1
|
|
266
268
|
}
|
|
267
269
|
return res
|
|
268
270
|
}
|
|
@@ -34,36 +34,28 @@ describe('DRBG', () => {
|
|
|
34
34
|
const entropy = new Array(31).fill(0x01)
|
|
35
35
|
const nonce = new Array(32).fill(0x02)
|
|
36
36
|
|
|
37
|
-
expect(() =>
|
|
38
|
-
new DRBG(entropy, nonce)
|
|
39
|
-
}).toThrow('Entropy must be exactly 32 bytes (256 bits)')
|
|
37
|
+
expect(() => new DRBG(entropy, nonce)).toThrow('Entropy must be exactly 32 bytes (256 bits)')
|
|
40
38
|
})
|
|
41
39
|
|
|
42
40
|
it('throws if entropy is longer than 32 bytes', () => {
|
|
43
41
|
const entropy = new Array(33).fill(0x01)
|
|
44
42
|
const nonce = new Array(32).fill(0x02)
|
|
45
43
|
|
|
46
|
-
expect(() =>
|
|
47
|
-
new DRBG(entropy, nonce)
|
|
48
|
-
}).toThrow('Entropy must be exactly 32 bytes (256 bits)')
|
|
44
|
+
expect(() => new DRBG(entropy, nonce)).toThrow('Entropy must be exactly 32 bytes (256 bits)')
|
|
49
45
|
})
|
|
50
46
|
|
|
51
47
|
it('throws if nonce is shorter than 32 bytes', () => {
|
|
52
48
|
const entropy = new Array(32).fill(0x01)
|
|
53
49
|
const nonce = new Array(31).fill(0x02)
|
|
54
50
|
|
|
55
|
-
expect(() =>
|
|
56
|
-
new DRBG(entropy, nonce)
|
|
57
|
-
}).toThrow('Nonce must be exactly 32 bytes (256 bits)')
|
|
51
|
+
expect(() => new DRBG(entropy, nonce)).toThrow('Nonce must be exactly 32 bytes (256 bits)')
|
|
58
52
|
})
|
|
59
53
|
|
|
60
54
|
it('throws if nonce is longer than 32 bytes', () => {
|
|
61
55
|
const entropy = new Array(32).fill(0x01)
|
|
62
56
|
const nonce = new Array(33).fill(0x02)
|
|
63
57
|
|
|
64
|
-
expect(() =>
|
|
65
|
-
new DRBG(entropy, nonce)
|
|
66
|
-
}).toThrow('Nonce must be exactly 32 bytes (256 bits)')
|
|
58
|
+
expect(() => new DRBG(entropy, nonce)).toThrow('Nonce must be exactly 32 bytes (256 bits)')
|
|
67
59
|
})
|
|
68
60
|
|
|
69
61
|
it('accepts both hex strings and number[] inputs equivalently', () => {
|
|
@@ -118,7 +118,7 @@ describe('Point – additional coverage', () => {
|
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
it('throws for unknown format', () => {
|
|
121
|
-
const der = [0x05, ...Array(32).fill(0x01)]
|
|
121
|
+
const der = [0x05, ...new Array(32).fill(0x01)]
|
|
122
122
|
expect(() => Point.fromDER(der)).toThrow('Unknown point format')
|
|
123
123
|
})
|
|
124
124
|
})
|
|
@@ -147,7 +147,7 @@ describe('Point – additional coverage', () => {
|
|
|
147
147
|
it('fromX accepts number', () => {
|
|
148
148
|
// Use a valid x value that has a square root mod p
|
|
149
149
|
const g = G.mul(new BigNumber(7))
|
|
150
|
-
const xNum = parseInt(g.getX().toString(16).slice(-4), 16)
|
|
150
|
+
const xNum = Number.parseInt(g.getX().toString(16).slice(-4), 16)
|
|
151
151
|
// fromX with a number, may produce a point
|
|
152
152
|
const p = Point.fromX(g.getX(), true)
|
|
153
153
|
expect(p.validate()).toBe(true)
|
|
@@ -40,7 +40,7 @@ describe('Point.fromJSON / fromDER / fromX curve validation (TOB-24)', () => {
|
|
|
40
40
|
it('rejects invalid compressed points in fromDER', () => {
|
|
41
41
|
// 0x02 is a valid compressed prefix, but x = 0 gives y^2 = 7,
|
|
42
42
|
// which has no square root mod p on secp256k1 → invalid point.
|
|
43
|
-
const der = [0x02, ...Array(32).fill(0x00)]
|
|
43
|
+
const der = [0x02, ...new Array(32).fill(0x00)]
|
|
44
44
|
expect(() => Point.fromDER(der)).toThrow(/Invalid point/)
|
|
45
45
|
})
|
|
46
46
|
|
|
@@ -37,7 +37,7 @@ describe('Cross-SDK Compatibility Tests', () => {
|
|
|
37
37
|
// Convert hex to byte array
|
|
38
38
|
const ciphertext: number[] = []
|
|
39
39
|
for (let i = 0; i < ciphertextHex.length; i += 2) {
|
|
40
|
-
ciphertext.push(parseInt(ciphertextHex.slice(i, i + 2), 16))
|
|
40
|
+
ciphertext.push(Number.parseInt(ciphertextHex.slice(i, i + 2), 16))
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Decrypt using TypeScript SDK
|
|
@@ -83,7 +83,7 @@ describe('Cross-SDK Compatibility Tests', () => {
|
|
|
83
83
|
// Convert hex to byte array
|
|
84
84
|
const ciphertext: number[] = []
|
|
85
85
|
for (let i = 0; i < ciphertextHex.length; i += 2) {
|
|
86
|
-
ciphertext.push(parseInt(ciphertextHex.slice(i, i + 2), 16))
|
|
86
|
+
ciphertext.push(Number.parseInt(ciphertextHex.slice(i, i + 2), 16))
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// Decrypt using TypeScript SDK
|
|
@@ -127,7 +127,7 @@ describe('Cross-SDK Compatibility Tests', () => {
|
|
|
127
127
|
// Test TypeScript decrypting Go ciphertext
|
|
128
128
|
const goCiphertextBytes: number[] = []
|
|
129
129
|
for (let i = 0; i < goCiphertext.length; i += 2) {
|
|
130
|
-
goCiphertextBytes.push(parseInt(goCiphertext.substr(i, 2), 16))
|
|
130
|
+
goCiphertextBytes.push(Number.parseInt(goCiphertext.substr(i, 2), 16))
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const goDecrypted = symKey.decrypt(goCiphertextBytes, 'utf8')
|
|
@@ -136,7 +136,7 @@ describe('Cross-SDK Compatibility Tests', () => {
|
|
|
136
136
|
// Test TypeScript decrypting TypeScript ciphertext (sanity check)
|
|
137
137
|
const tsCiphertextBytes: number[] = []
|
|
138
138
|
for (let i = 0; i < tsCiphertext.length; i += 2) {
|
|
139
|
-
tsCiphertextBytes.push(parseInt(tsCiphertext.substr(i, 2), 16))
|
|
139
|
+
tsCiphertextBytes.push(Number.parseInt(tsCiphertext.substr(i, 2), 16))
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
const tsDecrypted = symKey.decrypt(tsCiphertextBytes, 'utf8')
|
|
@@ -77,10 +77,10 @@ jest.mock('../../transaction/index.js', () => ({
|
|
|
77
77
|
jest.mock('../../primitives/index.js', () => ({
|
|
78
78
|
Utils: {
|
|
79
79
|
toArray: jest.fn().mockImplementation((str: string) =>
|
|
80
|
-
Array.from(str).map((c) => c.
|
|
80
|
+
Array.from(str).map((c) => c.codePointAt(0))
|
|
81
81
|
),
|
|
82
82
|
toUTF8: jest.fn().mockImplementation((arr: number[] | string) => {
|
|
83
|
-
if (Array.isArray(arr)) return arr.map((n) => String.
|
|
83
|
+
if (Array.isArray(arr)) return arr.map((n) => String.fromCodePoint(n)).join('')
|
|
84
84
|
return arr
|
|
85
85
|
})
|
|
86
86
|
}
|
|
@@ -628,7 +628,7 @@ describe('RegistryClient.resolve – protocol and certificate parsing', () => {
|
|
|
628
628
|
// protocol has 7 fields: protocolID, name, iconURL, description, docURL, operator, sig
|
|
629
629
|
;(PushDrop.decode as jest.Mock).mockReturnValueOnce({
|
|
630
630
|
fields: [
|
|
631
|
-
Array.from('[1,"proto"]').map((c) => c.
|
|
631
|
+
Array.from('[1,"proto"]').map((c) => c.codePointAt(0)), // protocolID JSON
|
|
632
632
|
[110, 97, 109, 101], // 'name'
|
|
633
633
|
[105, 99, 111, 110], // 'icon'
|
|
634
634
|
[100, 101, 115, 99], // 'desc'
|
|
@@ -658,7 +658,7 @@ describe('RegistryClient.resolve – protocol and certificate parsing', () => {
|
|
|
658
658
|
[105, 99, 111, 110], // 'icon'
|
|
659
659
|
[100, 101, 115, 99], // 'desc'
|
|
660
660
|
[100, 111, 99], // 'doc'
|
|
661
|
-
Array.from(fieldsJSON).map((c) => c.
|
|
661
|
+
Array.from(fieldsJSON).map((c) => c.codePointAt(0)), // fieldsJSON
|
|
662
662
|
[111, 112], // 'op' - operator
|
|
663
663
|
[115, 105, 103] // signature
|
|
664
664
|
]
|
|
@@ -82,11 +82,11 @@ jest.mock('../../primitives/index.js', () => {
|
|
|
82
82
|
return {
|
|
83
83
|
Utils: {
|
|
84
84
|
toArray: jest.fn().mockImplementation((str: string) =>
|
|
85
|
-
Array.from(str).map((c) => c.
|
|
85
|
+
Array.from(str).map((c) => c.codePointAt(0))
|
|
86
86
|
),
|
|
87
87
|
toUTF8: jest.fn().mockImplementation((arr: number[] | string) => {
|
|
88
88
|
if (Array.isArray(arr)) {
|
|
89
|
-
return arr.map((n) => String.
|
|
89
|
+
return arr.map((n) => String.fromCodePoint(n)).join('')
|
|
90
90
|
}
|
|
91
91
|
return arr
|
|
92
92
|
})
|
package/src/script/Spend.ts
CHANGED
|
@@ -1213,7 +1213,14 @@ export default class Spend {
|
|
|
1213
1213
|
sig = this.parseChecksigSignature(bufSig)
|
|
1214
1214
|
|
|
1215
1215
|
const scriptForChecksig: Script = this.context === 'UnlockingScript' ? this.unlockingScript : this.lockingScript
|
|
1216
|
-
|
|
1216
|
+
let scriptCodeChunks = scriptForChecksig.chunks.slice(this.lastCodeSeparator === null ? 0 : this.lastCodeSeparator + 1)
|
|
1217
|
+
// When an OP_CODESEPARATOR appears in the unlocking script, the CHECKSIG subscript
|
|
1218
|
+
// continues across the unlock/lock boundary into the full locking script (legacy
|
|
1219
|
+
// combined-script semantics; matches BSV node consensus). Without this, signatures
|
|
1220
|
+
// taken over such a subscript (e.g. OP_PUSH_TX-style contracts) are wrongly rejected.
|
|
1221
|
+
if (this.context === 'UnlockingScript') {
|
|
1222
|
+
scriptCodeChunks = scriptCodeChunks.concat(this.lockingScript.chunks)
|
|
1223
|
+
}
|
|
1217
1224
|
subscript = new Script(scriptCodeChunks)
|
|
1218
1225
|
subscript.findAndDelete(new Script().writeBin(bufSig))
|
|
1219
1226
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import PrivateKey from '../../primitives/PrivateKey'
|
|
2
|
+
import { sha256 } from '../../primitives/Hash'
|
|
3
|
+
import Spend from '../../script/Spend'
|
|
4
|
+
import Transaction from '../../transaction/Transaction'
|
|
5
|
+
import TransactionSignature from '../../primitives/TransactionSignature'
|
|
6
|
+
import LockingScript from '../../script/LockingScript'
|
|
7
|
+
import UnlockingScript from '../../script/UnlockingScript'
|
|
8
|
+
import Script from '../../script/Script'
|
|
9
|
+
import OP from '../../script/OP'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Regression test for the CHECKSIG subscript across an OP_CODESEPARATOR in the unlocking script.
|
|
13
|
+
*
|
|
14
|
+
* When an OP_CODESEPARATOR is executed in the *unlocking* script and an OP_CHECKSIG then runs
|
|
15
|
+
* (still in unlocking-script context), the signature's subscript must span from after that
|
|
16
|
+
* separator across the unlock/lock boundary into the FULL locking script (legacy combined-script
|
|
17
|
+
* semantics, which BSV nodes enforce). This is the basis of OP_PUSH_TX-style contracts.
|
|
18
|
+
*
|
|
19
|
+
* Before the fix, Spend built the subscript from the unlocking script alone, so a signature taken
|
|
20
|
+
* over <unlock-tail> ++ <locking-script> was rejected ("OP_CHECKSIGVERIFY requires a valid
|
|
21
|
+
* signature"), even though BSV consensus accepts the transaction.
|
|
22
|
+
*/
|
|
23
|
+
describe('Spend — CHECKSIG subscript across OP_CODESEPARATOR in the unlocking script', () => {
|
|
24
|
+
it('validates a signature whose subscript spans into the locking script', () => {
|
|
25
|
+
const priv = new PrivateKey(42)
|
|
26
|
+
const pub = priv.toPublicKey()
|
|
27
|
+
const pubEnc = pub.encode(true) as number[]
|
|
28
|
+
const satoshis = 1000
|
|
29
|
+
const scope = TransactionSignature.SIGHASH_ALL | TransactionSignature.SIGHASH_FORKID
|
|
30
|
+
|
|
31
|
+
// Non-empty locking-script tail that MUST be part of the subscript (proves the concat).
|
|
32
|
+
const lockingScript = new LockingScript([{ op: OP.OP_NOP }, { op: OP.OP_NOP }])
|
|
33
|
+
|
|
34
|
+
// Version 2 -> "relaxed" rules (post-Genesis/Chronicle), so the non-push unlocking script
|
|
35
|
+
// below is permitted (push-only is only enforced for legacy v1 transactions).
|
|
36
|
+
const sourceTx = new Transaction(2, [], [{ lockingScript, satoshis }], 0)
|
|
37
|
+
|
|
38
|
+
// The subscript the interpreter derives (after findAndDelete removes the signature push):
|
|
39
|
+
// <unlock-after-codesep without the sig> ++ <locking script>
|
|
40
|
+
// = [ <pubkey> OP_CHECKSIG ] ++ [ OP_NOP OP_NOP ]
|
|
41
|
+
const subscript = new Script([
|
|
42
|
+
{ op: pubEnc.length, data: pubEnc },
|
|
43
|
+
{ op: OP.OP_CHECKSIG },
|
|
44
|
+
...lockingScript.chunks
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
const preimage = TransactionSignature.formatBytes({
|
|
48
|
+
sourceTXID: sourceTx.id('hex'),
|
|
49
|
+
sourceOutputIndex: 0,
|
|
50
|
+
sourceSatoshis: satoshis,
|
|
51
|
+
transactionVersion: 2,
|
|
52
|
+
otherInputs: [],
|
|
53
|
+
outputs: [],
|
|
54
|
+
inputIndex: 0,
|
|
55
|
+
subscript,
|
|
56
|
+
inputSequence: 0xffffffff,
|
|
57
|
+
lockTime: 0,
|
|
58
|
+
scope
|
|
59
|
+
})
|
|
60
|
+
const raw = priv.sign(sha256(preimage))
|
|
61
|
+
const sig = new TransactionSignature(raw.r, raw.s, scope)
|
|
62
|
+
const sigForScript = sig.toChecksigFormat()
|
|
63
|
+
|
|
64
|
+
// Unlocking script: OP_CODESEPARATOR then <sig> <pubkey> OP_CHECKSIG (checksig runs here).
|
|
65
|
+
const unlockingScript = new UnlockingScript([
|
|
66
|
+
{ op: OP.OP_CODESEPARATOR },
|
|
67
|
+
{ op: sigForScript.length, data: sigForScript },
|
|
68
|
+
{ op: pubEnc.length, data: pubEnc },
|
|
69
|
+
{ op: OP.OP_CHECKSIG }
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
const spend = new Spend({
|
|
73
|
+
sourceTXID: sourceTx.id('hex'),
|
|
74
|
+
sourceOutputIndex: 0,
|
|
75
|
+
sourceSatoshis: satoshis,
|
|
76
|
+
lockingScript,
|
|
77
|
+
transactionVersion: 2,
|
|
78
|
+
otherInputs: [],
|
|
79
|
+
inputIndex: 0,
|
|
80
|
+
unlockingScript,
|
|
81
|
+
outputs: [],
|
|
82
|
+
inputSequence: 0xffffffff,
|
|
83
|
+
lockTime: 0
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(spend.validate()).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
})
|
|
@@ -376,7 +376,7 @@ describe('StorageUploader — multi-provider behavior', () => {
|
|
|
376
376
|
})
|
|
377
377
|
|
|
378
378
|
// All three quotes requested (up to 2 * resilienceLevel = 4 allowed, but we only have 3 hosts).
|
|
379
|
-
expect(quoteCalls.sort()).toEqual([
|
|
379
|
+
expect(quoteCalls.sort((a, b) => a.localeCompare(b))).toEqual([
|
|
380
380
|
'https://a.example/quote',
|
|
381
381
|
'https://b.example/quote',
|
|
382
382
|
'https://c.example/quote'
|
|
@@ -611,7 +611,7 @@ describe('StorageUploader — multi-provider behavior', () => {
|
|
|
611
611
|
})
|
|
612
612
|
|
|
613
613
|
// Only the first batch of 4 quote requests should have been issued.
|
|
614
|
-
expect(quoteCalls.sort()).toEqual([
|
|
614
|
+
expect(quoteCalls.sort((a, b) => a.localeCompare(b))).toEqual([
|
|
615
615
|
'https://h1.example/quote',
|
|
616
616
|
'https://h2.example/quote',
|
|
617
617
|
'https://h3.example/quote',
|
|
@@ -649,7 +649,7 @@ describe('StorageUploader — multi-provider behavior', () => {
|
|
|
649
649
|
retentionPeriod: 60
|
|
650
650
|
})
|
|
651
651
|
|
|
652
|
-
expect(quoteCalls.sort()).toEqual([
|
|
652
|
+
expect(quoteCalls.sort((a, b) => a.localeCompare(b))).toEqual([
|
|
653
653
|
'https://h1.example/quote',
|
|
654
654
|
'https://h2.example/quote',
|
|
655
655
|
'https://h3.example/quote',
|
|
@@ -696,7 +696,7 @@ describe('StorageUploader — multi-provider behavior', () => {
|
|
|
696
696
|
|
|
697
697
|
// First batch queried h1-h4 (4 hosts), second batch queried only h5
|
|
698
698
|
// (1 host, the exact remainder). Hosts h6-h8 are never contacted.
|
|
699
|
-
expect(quoteCalls.sort()).toEqual([
|
|
699
|
+
expect(quoteCalls.sort((a, b) => a.localeCompare(b))).toEqual([
|
|
700
700
|
'https://h1.example/quote',
|
|
701
701
|
'https://h2.example/quote',
|
|
702
702
|
'https://h3.example/quote',
|
|
@@ -763,7 +763,7 @@ describe('StorageUploader — multi-host findFile / listUploads / renewFile', ()
|
|
|
763
763
|
|
|
764
764
|
const result = await uploader.findFile('uhrp://x')
|
|
765
765
|
|
|
766
|
-
expect(calls.sort()).toEqual([
|
|
766
|
+
expect(calls.sort((a, b) => a.localeCompare(b))).toEqual([
|
|
767
767
|
'https://a.example/find?uhrpUrl=uhrp%3A%2F%2Fx',
|
|
768
768
|
'https://b.example/find?uhrpUrl=uhrp%3A%2F%2Fx',
|
|
769
769
|
'https://c.example/find?uhrpUrl=uhrp%3A%2F%2Fx'
|
|
@@ -843,7 +843,7 @@ describe('StorageUploader — multi-host findFile / listUploads / renewFile', ()
|
|
|
843
843
|
const listing = await uploader.listUploads()
|
|
844
844
|
const byUrl = Object.fromEntries(listing.map((e: any) => [e.uhrpUrl, e]))
|
|
845
845
|
|
|
846
|
-
expect(Object.keys(byUrl).sort()).toEqual(['uhrp://one', 'uhrp://shared', 'uhrp://two'])
|
|
846
|
+
expect(Object.keys(byUrl).sort((a, b) => a.localeCompare(b))).toEqual(['uhrp://one', 'uhrp://shared', 'uhrp://two'])
|
|
847
847
|
expect(byUrl['uhrp://shared'].expiryTime).toBe(300) // longest wins
|
|
848
848
|
expect(byUrl['uhrp://shared'].hostedBy.sort()).toEqual(['https://a.example', 'https://b.example'])
|
|
849
849
|
expect(byUrl['uhrp://one'].hostedBy).toEqual(['https://a.example'])
|
|
@@ -1026,7 +1026,7 @@ describe('StorageUploader — multi-host findFile / listUploads / renewFile', ()
|
|
|
1026
1026
|
hostedBy: ['https://a.example', 'https://c.example']
|
|
1027
1027
|
})
|
|
1028
1028
|
|
|
1029
|
-
expect(calls.sort()).toEqual([
|
|
1029
|
+
expect(calls.sort((a, b) => a.localeCompare(b))).toEqual([
|
|
1030
1030
|
'https://a.example/renew',
|
|
1031
1031
|
'https://c.example/renew'
|
|
1032
1032
|
])
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type Transaction from './Transaction.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A pluggable backend that verifies ALL input scripts of a single transaction.
|
|
5
|
+
*
|
|
6
|
+
* Implementations (e.g. @bsv/verifast's BdkVerifier) typically delegate to a
|
|
7
|
+
* native/WASM engine. The backend operates at whole-transaction granularity,
|
|
8
|
+
* not per input.
|
|
9
|
+
*/
|
|
10
|
+
export default interface BdkVerifierInterface {
|
|
11
|
+
/**
|
|
12
|
+
* Verify all input scripts of `params.tx`.
|
|
13
|
+
* @returns Promise resolving true if every input script is valid, false otherwise.
|
|
14
|
+
* @throws If the backend itself fails (load error, marshalling error, unavailable).
|
|
15
|
+
*/
|
|
16
|
+
verifyScripts: (params: {
|
|
17
|
+
tx: Transaction
|
|
18
|
+
blockHeight: number
|
|
19
|
+
consensus: boolean
|
|
20
|
+
verifyFlags?: string | string[]
|
|
21
|
+
}) => Promise<boolean>
|
|
22
|
+
}
|
|
@@ -18,6 +18,10 @@ import P2PKH from '../script/templates/P2PKH.js'
|
|
|
18
18
|
import type { WalletInterface, DescriptionString5to50Bytes, CreateActionOptions } from '../wallet/Wallet.interfaces.js'
|
|
19
19
|
import TransactionSignature from '../primitives/TransactionSignature.js'
|
|
20
20
|
import Random from '../primitives/Random.js'
|
|
21
|
+
import type BdkVerifierInterface from './BdkVerifierInterface.js'
|
|
22
|
+
|
|
23
|
+
/** Post-Chronicle height used when an input's source UTXO mined-height is unobtainable. */
|
|
24
|
+
const POST_CHRONICLE_HEIGHT_FALLBACK = 943816
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Represents a complete Bitcoin transaction. This class encapsulates all the details
|
|
@@ -833,7 +837,8 @@ export default class Transaction {
|
|
|
833
837
|
async verify (
|
|
834
838
|
chainTracker: ChainTracker | 'scripts only' = defaultChainTracker(),
|
|
835
839
|
feeModel?: FeeModel,
|
|
836
|
-
memoryLimit?: number
|
|
840
|
+
memoryLimit?: number,
|
|
841
|
+
verifier?: BdkVerifierInterface
|
|
837
842
|
): Promise<boolean> {
|
|
838
843
|
const verifiedTxids = new Set<string>()
|
|
839
844
|
const txQueue: Transaction[] = [this]
|
|
@@ -910,23 +915,41 @@ export default class Transaction {
|
|
|
910
915
|
const otherInputs = tx.inputs.filter((_, idx) => idx !== i)
|
|
911
916
|
input.sourceTXID ??= sourceTxid
|
|
912
917
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
918
|
+
if (verifier === undefined) {
|
|
919
|
+
const spend = new Spend({
|
|
920
|
+
sourceTXID: input.sourceTXID,
|
|
921
|
+
sourceOutputIndex: input.sourceOutputIndex,
|
|
922
|
+
lockingScript: sourceOutput.lockingScript,
|
|
923
|
+
sourceSatoshis: sourceOutput.satoshis ?? 0,
|
|
924
|
+
transactionVersion: tx.version,
|
|
925
|
+
otherInputs,
|
|
926
|
+
unlockingScript: input.unlockingScript,
|
|
927
|
+
inputSequence: input.sequence ?? 0xffffffff, // default to max sequence
|
|
928
|
+
inputIndex: i,
|
|
929
|
+
outputs: tx.outputs,
|
|
930
|
+
lockTime: tx.lockTime,
|
|
931
|
+
memoryLimit
|
|
932
|
+
})
|
|
933
|
+
const spendValid = spend.validate()
|
|
934
|
+
|
|
935
|
+
if (!spendValid) {
|
|
936
|
+
return false
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
928
940
|
|
|
929
|
-
|
|
941
|
+
// When a pluggable verifier is configured, hand the whole transaction to it
|
|
942
|
+
// once (BDK operates at whole-tx granularity). Strict: its verdict is
|
|
943
|
+
// authoritative and any thrown error propagates (no JS fallback).
|
|
944
|
+
if (verifier !== undefined) {
|
|
945
|
+
// A tx reaching here has no merkle proof (mined txs short-circuit above),
|
|
946
|
+
// so its source UTXO mined-height is unobtainable -> post-Chronicle fallback.
|
|
947
|
+
const scriptsValid = await verifier.verifyScripts({
|
|
948
|
+
tx,
|
|
949
|
+
blockHeight: POST_CHRONICLE_HEIGHT_FALLBACK,
|
|
950
|
+
consensus: true
|
|
951
|
+
})
|
|
952
|
+
if (!scriptsValid) {
|
|
930
953
|
return false
|
|
931
954
|
}
|
|
932
955
|
}
|