@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,837 @@
|
|
|
1
|
+
import Spend from '../../script/Spend'
|
|
2
|
+
import LockingScript from '../../script/LockingScript'
|
|
3
|
+
import UnlockingScript from '../../script/UnlockingScript'
|
|
4
|
+
import BigNumber from '../../primitives/BigNumber'
|
|
5
|
+
import OP from '../../script/OP'
|
|
6
|
+
import ScriptChunk from '../../script/ScriptChunk'
|
|
7
|
+
import PrivateKey from '../../primitives/PrivateKey'
|
|
8
|
+
import PublicKey from '../../primitives/PublicKey'
|
|
9
|
+
import Transaction from '../../transaction/Transaction'
|
|
10
|
+
import P2PKH from '../../script/templates/P2PKH'
|
|
11
|
+
|
|
12
|
+
const ZERO_TXID = '0'.repeat(64)
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helpers
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
function makeSpend (
|
|
19
|
+
lockingChunks: ScriptChunk[],
|
|
20
|
+
unlockingChunks: ScriptChunk[] = [],
|
|
21
|
+
opts: { transactionVersion?: number, memoryLimit?: number, isRelaxed?: boolean } = {}
|
|
22
|
+
): Spend {
|
|
23
|
+
return new Spend({
|
|
24
|
+
sourceTXID: ZERO_TXID,
|
|
25
|
+
sourceOutputIndex: 0,
|
|
26
|
+
sourceSatoshis: 1,
|
|
27
|
+
lockingScript: new LockingScript(lockingChunks),
|
|
28
|
+
transactionVersion: opts.transactionVersion ?? 1,
|
|
29
|
+
otherInputs: [],
|
|
30
|
+
outputs: [],
|
|
31
|
+
inputIndex: 0,
|
|
32
|
+
unlockingScript: new UnlockingScript(unlockingChunks),
|
|
33
|
+
inputSequence: 0xffffffff,
|
|
34
|
+
lockTime: 0,
|
|
35
|
+
memoryLimit: opts.memoryLimit,
|
|
36
|
+
isRelaxed: opts.isRelaxed
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Minimal-push helper that respects script number semantics. */
|
|
41
|
+
function pushChunk (data: number[]): ScriptChunk {
|
|
42
|
+
if (data.length === 0) return { op: OP.OP_0 }
|
|
43
|
+
if (data.length === 1 && data[0] >= 1 && data[0] <= 16) {
|
|
44
|
+
return { op: OP.OP_1 + (data[0] - 1) }
|
|
45
|
+
}
|
|
46
|
+
if (data.length === 1 && data[0] === 0x81) return { op: OP.OP_1NEGATE }
|
|
47
|
+
if (data.length <= 75) return { op: data.length, data }
|
|
48
|
+
if (data.length <= 255) return { op: OP.OP_PUSHDATA1, data }
|
|
49
|
+
if (data.length <= 65535) return { op: OP.OP_PUSHDATA2, data }
|
|
50
|
+
return { op: OP.OP_PUSHDATA4, data }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** scriptnum bytes for an integer. */
|
|
54
|
+
function scriptNum (n: number): number[] {
|
|
55
|
+
return new BigNumber(n).toScriptNum()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Fast way to make a Spend in relaxed mode with a locking-script-only test. */
|
|
59
|
+
function makeLocking (
|
|
60
|
+
lockingChunks: ScriptChunk[],
|
|
61
|
+
opts: { memoryLimit?: number } = {}
|
|
62
|
+
): Spend {
|
|
63
|
+
return makeSpend(lockingChunks, [], { isRelaxed: true, memoryLimit: opts.memoryLimit })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Memory limit checks (lines 238, 246, 383-384, 387-388)
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
describe('Spend – memory limit enforcement', () => {
|
|
70
|
+
it('step() throws when stackMem already exceeds memoryLimit', () => {
|
|
71
|
+
const spend = makeLocking([{ op: OP.OP_1 }], { memoryLimit: 0 })
|
|
72
|
+
spend.context = 'LockingScript'
|
|
73
|
+
spend.stackMem = 1 // artificially exceeded
|
|
74
|
+
expect(() => spend.step()).toThrow('Stack memory usage has exceeded')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('step() throws when altStackMem already exceeds memoryLimit', () => {
|
|
78
|
+
const spend = makeLocking([{ op: OP.OP_1 }], { memoryLimit: 0 })
|
|
79
|
+
spend.context = 'LockingScript'
|
|
80
|
+
spend.altStackMem = 1
|
|
81
|
+
expect(() => spend.step()).toThrow('Alt stack memory usage has exceeded')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('pushStack throws when additional bytes would exceed memoryLimit', () => {
|
|
85
|
+
// OP_1 calls pushStackCopy with a 1-byte item; memoryLimit=0 so 0+1>0
|
|
86
|
+
const spend = makeLocking([{ op: OP.OP_1 }], { memoryLimit: 0 })
|
|
87
|
+
spend.context = 'LockingScript'
|
|
88
|
+
expect(() => spend.step()).toThrow('Stack memory usage has exceeded')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('pushAltStack throws when additional bytes would exceed memoryLimit', () => {
|
|
92
|
+
const spend = makeLocking([{ op: OP.OP_TOALTSTACK }], { memoryLimit: 0 })
|
|
93
|
+
spend.context = 'LockingScript'
|
|
94
|
+
// Pre-populate stack without going through ensureStackMem
|
|
95
|
+
spend.stack = [[1]]
|
|
96
|
+
spend.stackMem = 0 // bypass step() initial guard (0 > 0 = false)
|
|
97
|
+
expect(() => spend.step()).toThrow('Alt stack memory usage has exceeded')
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// step() structural edge-cases (lines 401, 407, 417)
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
describe('Spend – step() structural edge-cases', () => {
|
|
105
|
+
it('step() returns false when locking script is exhausted', () => {
|
|
106
|
+
// Empty locking script: after unlocking script finishes, step() returns false
|
|
107
|
+
const spend = makeSpend([], [pushChunk([1])], { isRelaxed: true })
|
|
108
|
+
expect(spend.step()).toBe(true) // process unlocking push
|
|
109
|
+
expect(spend.step()).toBe(false) // locking is empty → return false
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('step() throws for non-minimal push in strict mode', () => {
|
|
113
|
+
// data=[0x01] should be pushed with OP_1 (0x51), not op=1
|
|
114
|
+
const nonMinimal: ScriptChunk = { op: 1, data: [0x01] }
|
|
115
|
+
const spend = makeSpend([nonMinimal], [], { memoryLimit: 1000 })
|
|
116
|
+
spend.context = 'LockingScript'
|
|
117
|
+
expect(() => spend.step()).toThrow('not minimally-encoded')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('step() processes non-minimal push when isRelaxed=true', () => {
|
|
121
|
+
const nonMinimal: ScriptChunk = { op: 1, data: [0x01] }
|
|
122
|
+
const spend = makeSpend([nonMinimal], [], { isRelaxed: true, memoryLimit: 1000 })
|
|
123
|
+
spend.context = 'LockingScript'
|
|
124
|
+
expect(spend.step()).toBe(true)
|
|
125
|
+
expect(spend.stack[0]).toEqual([0x01])
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// validate() error paths (lines 1103, 1118, 1125, 1132, 1136)
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
describe('Spend – validate() error paths', () => {
|
|
133
|
+
it('throws when unlocking script is not push-only in strict mode', () => {
|
|
134
|
+
// OP_DROP is not a push opcode
|
|
135
|
+
const spend = makeSpend(
|
|
136
|
+
[{ op: OP.OP_1 }],
|
|
137
|
+
[{ op: OP.OP_DROP }]
|
|
138
|
+
)
|
|
139
|
+
expect(() => spend.validate()).toThrow('Unlocking scripts can only contain push operations')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('throws when OP_IF is not closed with OP_ENDIF', () => {
|
|
143
|
+
// OP_1 OP_IF OP_1 ← no OP_ENDIF
|
|
144
|
+
const spend = makeLocking([
|
|
145
|
+
{ op: OP.OP_1 },
|
|
146
|
+
{ op: OP.OP_IF },
|
|
147
|
+
{ op: OP.OP_1 }
|
|
148
|
+
])
|
|
149
|
+
expect(() => spend.validate()).toThrow('OP_IF')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('throws clean-stack violation when more than one item left', () => {
|
|
153
|
+
// Non-relaxed mode: exactly 1 item is required
|
|
154
|
+
const spend = makeSpend([{ op: OP.OP_1 }, { op: OP.OP_1 }])
|
|
155
|
+
expect(() => spend.validate()).toThrow('clean stack')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('throws when stack is empty after execution', () => {
|
|
159
|
+
// OP_DROP leaves stack empty in relaxed mode
|
|
160
|
+
const spend = makeLocking([
|
|
161
|
+
{ op: OP.OP_1 },
|
|
162
|
+
{ op: OP.OP_DROP }
|
|
163
|
+
])
|
|
164
|
+
expect(() => spend.validate()).toThrow('stack is empty')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('throws when top stack item is falsy', () => {
|
|
168
|
+
const spend = makeLocking([{ op: OP.OP_0 }])
|
|
169
|
+
expect(() => spend.validate()).toThrow('top stack element must be truthy')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('castToBool returns false for negative-zero sentinel [0x80]', () => {
|
|
173
|
+
// Push [0x80] (negative zero) – should be falsy
|
|
174
|
+
const spend = makeLocking([pushChunk([0x80])])
|
|
175
|
+
expect(() => spend.validate()).toThrow('top stack element must be truthy')
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
// isChunkMinimalPushHelper – data.length 256-65535 path (line 97-98)
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
describe('Spend – isChunkMinimalPushHelper 256-65535 byte data', () => {
|
|
183
|
+
it('rejects PUSHDATA4 for 256-byte data (should use PUSHDATA2)', () => {
|
|
184
|
+
const data = new Array(256).fill(0x42)
|
|
185
|
+
// Using OP_PUSHDATA4 for 256-byte data is non-minimal (should be PUSHDATA2)
|
|
186
|
+
const badPush: ScriptChunk = { op: OP.OP_PUSHDATA4, data }
|
|
187
|
+
const spend = makeSpend([badPush], [], { memoryLimit: 10000000 })
|
|
188
|
+
spend.context = 'LockingScript'
|
|
189
|
+
expect(() => spend.step()).toThrow('not minimally-encoded')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('accepts PUSHDATA2 for 256-byte data', () => {
|
|
193
|
+
const data = new Array(256).fill(0x42)
|
|
194
|
+
const goodPush: ScriptChunk = { op: OP.OP_PUSHDATA2, data }
|
|
195
|
+
const spend = makeSpend([goodPush, { op: OP.OP_DROP }, { op: OP.OP_1 }], [], { memoryLimit: 10000000 })
|
|
196
|
+
expect(spend.validate()).toBe(true)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// checkPublicKeyEncoding paths (lines 328-353)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
describe('Spend – checkPublicKeyEncoding error paths', () => {
|
|
204
|
+
function checksigSpend (pubkeyBytes: number[], sigBytes: number[]): Spend {
|
|
205
|
+
return makeLocking([
|
|
206
|
+
pushChunk(sigBytes),
|
|
207
|
+
pushChunk(pubkeyBytes),
|
|
208
|
+
{ op: OP.OP_CHECKSIG }
|
|
209
|
+
])
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
it('throws when pubkey is empty', () => {
|
|
213
|
+
expect(() => checksigSpend([], []).validate()).toThrow('Public key is empty')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('throws when pubkey is too short (< 33 bytes)', () => {
|
|
217
|
+
const shortKey = new Array(32).fill(0x02)
|
|
218
|
+
expect(() => checksigSpend(shortKey, []).validate()).toThrow('too short')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('throws when 0x04 pubkey is not 65 bytes', () => {
|
|
222
|
+
const wrongLen = [0x04, ...new Array(33).fill(0x00)]
|
|
223
|
+
expect(() => checksigSpend(wrongLen, []).validate()).toThrow('non-compressed public key must be 65 bytes')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('throws when 0x02 pubkey is not 33 bytes', () => {
|
|
227
|
+
const wrongLen = [0x02, ...new Array(34).fill(0x00)]
|
|
228
|
+
expect(() => checksigSpend(wrongLen, []).validate()).toThrow('compressed public key must be 33 bytes')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('throws when 0x03 pubkey is not 33 bytes', () => {
|
|
232
|
+
const wrongLen = [0x03, ...new Array(34).fill(0x00)]
|
|
233
|
+
expect(() => checksigSpend(wrongLen, []).validate()).toThrow('compressed public key must be 33 bytes')
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('throws when pubkey prefix is unknown', () => {
|
|
237
|
+
const unknown = new Array(33).fill(0x00)
|
|
238
|
+
unknown[0] = 0x05
|
|
239
|
+
expect(() => checksigSpend(unknown, []).validate()).toThrow('unknown format')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('throws when pubkey prefix is 0x04 but coordinates are invalid', () => {
|
|
243
|
+
// 65-byte 0x04 with all zeros – valid length but invalid curve point
|
|
244
|
+
const bad = [0x04, ...new Array(64).fill(0x00)]
|
|
245
|
+
expect(() => checksigSpend(bad, []).validate()).toThrow()
|
|
246
|
+
})
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// checkSignatureEncoding paths (lines 310-321)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
describe('Spend – checkSignatureEncoding error paths', () => {
|
|
253
|
+
it('throws when sig has invalid DER format (wrong first byte)', () => {
|
|
254
|
+
const privKey = PrivateKey.fromRandom()
|
|
255
|
+
const pubKey = PublicKey.fromPrivateKey(privKey)
|
|
256
|
+
const badSig = [0x31, 0x06, 0x02, 0x01, 0x01, 0x02, 0x01, 0x01, 0x01]
|
|
257
|
+
const spend = makeLocking([
|
|
258
|
+
pushChunk(badSig),
|
|
259
|
+
pushChunk(pubKey.toDER() as number[]),
|
|
260
|
+
{ op: OP.OP_CHECKSIG }
|
|
261
|
+
])
|
|
262
|
+
expect(() => spend.validate()).toThrow('signature format is invalid')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('CHECKSIG with empty sig returns false result without error', () => {
|
|
266
|
+
const privKey = PrivateKey.fromRandom()
|
|
267
|
+
const pubKey = PublicKey.fromPrivateKey(privKey)
|
|
268
|
+
// empty sig → fSuccess stays false → pushes [] but still truthy check fails
|
|
269
|
+
const spend = makeLocking([
|
|
270
|
+
{ op: OP.OP_0 }, // empty sig
|
|
271
|
+
pushChunk(pubKey.toDER() as number[]),
|
|
272
|
+
{ op: OP.OP_CHECKSIG } // pushes [] (false)
|
|
273
|
+
// Stack is [[]] which is falsy → validate throws
|
|
274
|
+
])
|
|
275
|
+
expect(() => spend.validate()).toThrow('top stack element must be truthy')
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('CHECKSIGVERIFY throws when sig is empty (fSuccess = false)', () => {
|
|
279
|
+
const privKey = PrivateKey.fromRandom()
|
|
280
|
+
const pubKey = PublicKey.fromPrivateKey(privKey)
|
|
281
|
+
const spend = makeLocking([
|
|
282
|
+
{ op: OP.OP_0 }, // empty sig
|
|
283
|
+
pushChunk(pubKey.toDER() as number[]),
|
|
284
|
+
{ op: OP.OP_CHECKSIGVERIFY },
|
|
285
|
+
{ op: OP.OP_1 }
|
|
286
|
+
])
|
|
287
|
+
expect(() => spend.validate()).toThrow('OP_CHECKSIGVERIFY requires')
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('CHECKSIGVERIFY succeeds and pops result for valid sig', async () => {
|
|
291
|
+
const privKey = PrivateKey.fromRandom()
|
|
292
|
+
const pubKey = PublicKey.fromPrivateKey(privKey)
|
|
293
|
+
const p2pkh = new P2PKH()
|
|
294
|
+
const hash = pubKey.toHash()
|
|
295
|
+
const lockingScript = p2pkh.lock(hash)
|
|
296
|
+
const sourceTx = new Transaction(1, [], [{ lockingScript, satoshis: 1 }], 0)
|
|
297
|
+
const spendTx = new Transaction(
|
|
298
|
+
1,
|
|
299
|
+
[{ sourceTransaction: sourceTx, sourceOutputIndex: 0, sequence: 0xffffffff }],
|
|
300
|
+
[],
|
|
301
|
+
0
|
|
302
|
+
)
|
|
303
|
+
const unlockingScript = await p2pkh.unlock(privKey).sign(spendTx, 0)
|
|
304
|
+
const spend = new Spend({
|
|
305
|
+
sourceTXID: sourceTx.id('hex'),
|
|
306
|
+
sourceOutputIndex: 0,
|
|
307
|
+
sourceSatoshis: 1,
|
|
308
|
+
lockingScript,
|
|
309
|
+
transactionVersion: 1,
|
|
310
|
+
otherInputs: [],
|
|
311
|
+
outputs: [],
|
|
312
|
+
inputIndex: 0,
|
|
313
|
+
unlockingScript,
|
|
314
|
+
inputSequence: 0xffffffff,
|
|
315
|
+
lockTime: 0
|
|
316
|
+
})
|
|
317
|
+
expect(spend.validate()).toBe(true)
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// OP_CODESEPARATOR (lines 861-862)
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
describe('Spend – OP_CODESEPARATOR', () => {
|
|
325
|
+
it('sets lastCodeSeparator to current programCounter', () => {
|
|
326
|
+
const spend = makeLocking([
|
|
327
|
+
{ op: OP.OP_CODESEPARATOR },
|
|
328
|
+
{ op: OP.OP_1 }
|
|
329
|
+
])
|
|
330
|
+
expect(spend.validate()).toBe(true)
|
|
331
|
+
// lastCodeSeparator was set during execution
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
// ---------------------------------------------------------------------------
|
|
336
|
+
// OP_VER (lines 432-435)
|
|
337
|
+
// ---------------------------------------------------------------------------
|
|
338
|
+
describe('Spend – OP_VER', () => {
|
|
339
|
+
it('pushes transaction version as 4-byte LE', () => {
|
|
340
|
+
// transactionVersion=2, LE=[2,0,0,0]; compare with that value
|
|
341
|
+
const spend = makeLocking([
|
|
342
|
+
{ op: OP.OP_VER },
|
|
343
|
+
pushChunk([2, 0, 0, 0]),
|
|
344
|
+
{ op: OP.OP_EQUAL }
|
|
345
|
+
], { memoryLimit: 100000 })
|
|
346
|
+
// isRelaxed already set, transactionVersion defaults to 1 via makeLocking
|
|
347
|
+
// Redo with version=2
|
|
348
|
+
const spend2 = makeSpend([
|
|
349
|
+
{ op: OP.OP_VER },
|
|
350
|
+
pushChunk([2, 0, 0, 0]),
|
|
351
|
+
{ op: OP.OP_EQUAL }
|
|
352
|
+
], [], { isRelaxed: true, transactionVersion: 2, memoryLimit: 100000 })
|
|
353
|
+
expect(spend2.validate()).toBe(true)
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// OP_SUBSTR (lines 436-449)
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
describe('Spend – OP_SUBSTR', () => {
|
|
361
|
+
it('extracts a substring from a buffer', () => {
|
|
362
|
+
// "hello" starting at offset 1, length 3 → "ell"
|
|
363
|
+
const hello = [0x68, 0x65, 0x6c, 0x6c, 0x6f]
|
|
364
|
+
const ell = [0x65, 0x6c, 0x6c]
|
|
365
|
+
const spend = makeSpend([
|
|
366
|
+
pushChunk(hello),
|
|
367
|
+
pushChunk(scriptNum(1)),
|
|
368
|
+
pushChunk(scriptNum(3)),
|
|
369
|
+
{ op: OP.OP_SUBSTR },
|
|
370
|
+
pushChunk(ell),
|
|
371
|
+
{ op: OP.OP_EQUAL }
|
|
372
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
373
|
+
expect(spend.validate()).toBe(true)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it('throws when OP_SUBSTR offset is out of range', () => {
|
|
377
|
+
const buf = [0x01, 0x02, 0x03]
|
|
378
|
+
const spend = makeSpend([
|
|
379
|
+
pushChunk(buf),
|
|
380
|
+
pushChunk(scriptNum(5)), // offset >= size → error
|
|
381
|
+
pushChunk(scriptNum(1)),
|
|
382
|
+
{ op: OP.OP_SUBSTR },
|
|
383
|
+
{ op: OP.OP_1 }
|
|
384
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
385
|
+
expect(() => spend.validate()).toThrow('OP_SUBSTR')
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// OP_LEFT / OP_RIGHT (lines 450-474)
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
describe('Spend – OP_LEFT / OP_RIGHT', () => {
|
|
393
|
+
it('OP_LEFT extracts the first N bytes', () => {
|
|
394
|
+
const buf = [0x01, 0x02, 0x03, 0x04]
|
|
395
|
+
const spend = makeSpend([
|
|
396
|
+
pushChunk(buf),
|
|
397
|
+
pushChunk(scriptNum(2)),
|
|
398
|
+
{ op: OP.OP_LEFT },
|
|
399
|
+
pushChunk([0x01, 0x02]),
|
|
400
|
+
{ op: OP.OP_EQUAL }
|
|
401
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
402
|
+
expect(spend.validate()).toBe(true)
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
it('OP_LEFT throws when len is out of range', () => {
|
|
406
|
+
const buf = [0x01, 0x02]
|
|
407
|
+
const spend = makeSpend([
|
|
408
|
+
pushChunk(buf),
|
|
409
|
+
pushChunk(scriptNum(5)), // len > size → error
|
|
410
|
+
{ op: OP.OP_LEFT },
|
|
411
|
+
{ op: OP.OP_1 }
|
|
412
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
413
|
+
expect(() => spend.validate()).toThrow('OP_LEFT')
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('OP_RIGHT extracts the last N bytes', () => {
|
|
417
|
+
const buf = [0x01, 0x02, 0x03, 0x04]
|
|
418
|
+
const spend = makeSpend([
|
|
419
|
+
pushChunk(buf),
|
|
420
|
+
pushChunk(scriptNum(2)),
|
|
421
|
+
{ op: OP.OP_RIGHT },
|
|
422
|
+
pushChunk([0x03, 0x04]),
|
|
423
|
+
{ op: OP.OP_EQUAL }
|
|
424
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
425
|
+
expect(spend.validate()).toBe(true)
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
it('OP_RIGHT throws when len is out of range', () => {
|
|
429
|
+
const buf = [0x01, 0x02]
|
|
430
|
+
const spend = makeSpend([
|
|
431
|
+
pushChunk(buf),
|
|
432
|
+
pushChunk(scriptNum(5)),
|
|
433
|
+
{ op: OP.OP_RIGHT },
|
|
434
|
+
{ op: OP.OP_1 }
|
|
435
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
436
|
+
expect(() => spend.validate()).toThrow('OP_RIGHT')
|
|
437
|
+
})
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// OP_LSHIFTNUM / OP_RSHIFTNUM (lines 476-501)
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
describe('Spend – OP_LSHIFTNUM / OP_RSHIFTNUM', () => {
|
|
444
|
+
it('OP_LSHIFTNUM shifts a number left by N bits', () => {
|
|
445
|
+
// 1 << 3 = 8
|
|
446
|
+
const spend = makeSpend([
|
|
447
|
+
pushChunk(scriptNum(1)),
|
|
448
|
+
pushChunk(scriptNum(3)),
|
|
449
|
+
{ op: OP.OP_LSHIFTNUM },
|
|
450
|
+
pushChunk(scriptNum(8)),
|
|
451
|
+
{ op: OP.OP_NUMEQUAL }
|
|
452
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
453
|
+
expect(spend.validate()).toBe(true)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('OP_LSHIFTNUM throws when shift bits are negative', () => {
|
|
457
|
+
const spend = makeSpend([
|
|
458
|
+
pushChunk(scriptNum(1)),
|
|
459
|
+
pushChunk(scriptNum(-1)),
|
|
460
|
+
{ op: OP.OP_LSHIFTNUM },
|
|
461
|
+
{ op: OP.OP_1 }
|
|
462
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
463
|
+
expect(() => spend.validate()).toThrow('OP_LSHIFTNUM bits to shift must not be negative')
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('OP_RSHIFTNUM shifts a positive number right', () => {
|
|
467
|
+
// 8 >> 2 = 2
|
|
468
|
+
const spend = makeSpend([
|
|
469
|
+
pushChunk(scriptNum(8)),
|
|
470
|
+
pushChunk(scriptNum(2)),
|
|
471
|
+
{ op: OP.OP_RSHIFTNUM },
|
|
472
|
+
pushChunk(scriptNum(2)),
|
|
473
|
+
{ op: OP.OP_NUMEQUAL }
|
|
474
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
475
|
+
expect(spend.validate()).toBe(true)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('OP_RSHIFTNUM shifts a negative number right (sign-preserving)', () => {
|
|
479
|
+
// -8 >> 2 = -2
|
|
480
|
+
const spend = makeSpend([
|
|
481
|
+
pushChunk(scriptNum(-8)),
|
|
482
|
+
pushChunk(scriptNum(2)),
|
|
483
|
+
{ op: OP.OP_RSHIFTNUM },
|
|
484
|
+
pushChunk(scriptNum(-2)),
|
|
485
|
+
{ op: OP.OP_NUMEQUAL }
|
|
486
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
487
|
+
expect(spend.validate()).toBe(true)
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('OP_RSHIFTNUM throws when shift bits are negative', () => {
|
|
491
|
+
const spend = makeSpend([
|
|
492
|
+
pushChunk(scriptNum(8)),
|
|
493
|
+
pushChunk(scriptNum(-1)),
|
|
494
|
+
{ op: OP.OP_RSHIFTNUM },
|
|
495
|
+
{ op: OP.OP_1 }
|
|
496
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
497
|
+
expect(() => spend.validate()).toThrow('OP_RSHIFTNUM bits to shift must not be negative')
|
|
498
|
+
})
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// OP_1NEGATE (line 504-505)
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
describe('Spend – OP_1NEGATE', () => {
|
|
505
|
+
it('pushes -1 onto the stack', () => {
|
|
506
|
+
const spend = makeSpend([
|
|
507
|
+
{ op: OP.OP_1NEGATE },
|
|
508
|
+
pushChunk(scriptNum(-1)),
|
|
509
|
+
{ op: OP.OP_NUMEQUAL }
|
|
510
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
511
|
+
expect(spend.validate()).toBe(true)
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
// OP_VERIF / OP_VERNOTIF (lines 531-544)
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
describe('Spend – OP_VERIF / OP_VERNOTIF', () => {
|
|
519
|
+
it('OP_VERIF: matching 4-byte LE version makes inner block execute', () => {
|
|
520
|
+
// version=2 → LE = [2,0,0,0]; VERIF match → ifStack=true → inner push executes
|
|
521
|
+
const spend = makeSpend([
|
|
522
|
+
pushChunk([2, 0, 0, 0]),
|
|
523
|
+
{ op: OP.OP_VERIF },
|
|
524
|
+
{ op: OP.OP_1 },
|
|
525
|
+
{ op: OP.OP_ENDIF }
|
|
526
|
+
], [], { isRelaxed: true, transactionVersion: 2, memoryLimit: 100000 })
|
|
527
|
+
expect(spend.validate()).toBe(true)
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
it('OP_VERIF: non-matching version skips inner block', () => {
|
|
531
|
+
// version=2, push [3,0,0,0] → no match → ifStack=false → inner push skipped
|
|
532
|
+
const spend = makeSpend([
|
|
533
|
+
pushChunk([3, 0, 0, 0]),
|
|
534
|
+
{ op: OP.OP_VERIF },
|
|
535
|
+
{ op: OP.OP_1 },
|
|
536
|
+
{ op: OP.OP_ENDIF },
|
|
537
|
+
{ op: OP.OP_1 } // fallback truthy result
|
|
538
|
+
], [], { isRelaxed: true, transactionVersion: 2, memoryLimit: 100000 })
|
|
539
|
+
expect(spend.validate()).toBe(true)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it('OP_VERIF: non-4-byte value never matches', () => {
|
|
543
|
+
const spend = makeSpend([
|
|
544
|
+
pushChunk([1]), // 1-byte value → never 4-byte match
|
|
545
|
+
{ op: OP.OP_VERIF },
|
|
546
|
+
{ op: OP.OP_1 },
|
|
547
|
+
{ op: OP.OP_ENDIF },
|
|
548
|
+
{ op: OP.OP_1 }
|
|
549
|
+
], [], { isRelaxed: true, transactionVersion: 1, memoryLimit: 100000 })
|
|
550
|
+
expect(spend.validate()).toBe(true)
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it('OP_VERNOTIF: matching version skips inner block (negated)', () => {
|
|
554
|
+
// version=2, [2,0,0,0] matches → VERNOTIF negates → ifStack=false → block skipped
|
|
555
|
+
const spend = makeSpend([
|
|
556
|
+
pushChunk([2, 0, 0, 0]),
|
|
557
|
+
{ op: OP.OP_VERNOTIF },
|
|
558
|
+
{ op: OP.OP_1 },
|
|
559
|
+
{ op: OP.OP_ENDIF },
|
|
560
|
+
{ op: OP.OP_1 }
|
|
561
|
+
], [], { isRelaxed: true, transactionVersion: 2, memoryLimit: 100000 })
|
|
562
|
+
expect(spend.validate()).toBe(true)
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
// ---------------------------------------------------------------------------
|
|
567
|
+
// OP_IFDUP (lines 628-633)
|
|
568
|
+
// ---------------------------------------------------------------------------
|
|
569
|
+
describe('Spend – OP_IFDUP', () => {
|
|
570
|
+
it('duplicates top item when truthy', () => {
|
|
571
|
+
const spend = makeSpend([
|
|
572
|
+
{ op: OP.OP_1 },
|
|
573
|
+
{ op: OP.OP_IFDUP },
|
|
574
|
+
{ op: OP.OP_DROP } // consume duplicate
|
|
575
|
+
// stack: [1] → truthy → validate ok
|
|
576
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
577
|
+
expect(spend.validate()).toBe(true)
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it('does NOT duplicate top item when falsy', () => {
|
|
581
|
+
// [0x00] is falsy → IFDUP leaves stack unchanged → stack=[0x00] → falsy → throws
|
|
582
|
+
const spend = makeSpend([
|
|
583
|
+
pushChunk([0x00]),
|
|
584
|
+
{ op: OP.OP_IFDUP }
|
|
585
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
586
|
+
expect(() => spend.validate()).toThrow('top stack element must be truthy')
|
|
587
|
+
})
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
// ---------------------------------------------------------------------------
|
|
591
|
+
// OP_AND / OP_OR / OP_XOR / OP_INVERT (lines 704-726)
|
|
592
|
+
// ---------------------------------------------------------------------------
|
|
593
|
+
describe('Spend – bitwise opcodes', () => {
|
|
594
|
+
it('OP_AND performs bitwise AND', () => {
|
|
595
|
+
const spend = makeSpend([
|
|
596
|
+
pushChunk([0xff]),
|
|
597
|
+
pushChunk([0x0f]),
|
|
598
|
+
{ op: OP.OP_AND },
|
|
599
|
+
pushChunk([0x0f]),
|
|
600
|
+
{ op: OP.OP_EQUAL }
|
|
601
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
602
|
+
expect(spend.validate()).toBe(true)
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
it('OP_AND throws when operands differ in length', () => {
|
|
606
|
+
const spend = makeSpend([
|
|
607
|
+
pushChunk([0x01, 0x02]),
|
|
608
|
+
pushChunk([0x01]),
|
|
609
|
+
{ op: OP.OP_AND },
|
|
610
|
+
{ op: OP.OP_1 }
|
|
611
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
612
|
+
expect(() => spend.validate()).toThrow('OP_AND requires the top two stack items to be the same size')
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('OP_OR performs bitwise OR', () => {
|
|
616
|
+
const spend = makeSpend([
|
|
617
|
+
pushChunk([0xf0]),
|
|
618
|
+
pushChunk([0x0f]),
|
|
619
|
+
{ op: OP.OP_OR },
|
|
620
|
+
pushChunk([0xff]),
|
|
621
|
+
{ op: OP.OP_EQUAL }
|
|
622
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
623
|
+
expect(spend.validate()).toBe(true)
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
it('OP_XOR performs bitwise XOR', () => {
|
|
627
|
+
const spend = makeSpend([
|
|
628
|
+
pushChunk([0xff]),
|
|
629
|
+
pushChunk([0xf0]),
|
|
630
|
+
{ op: OP.OP_XOR },
|
|
631
|
+
pushChunk([0x0f]),
|
|
632
|
+
{ op: OP.OP_EQUAL }
|
|
633
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
634
|
+
expect(spend.validate()).toBe(true)
|
|
635
|
+
})
|
|
636
|
+
|
|
637
|
+
it('OP_INVERT performs bitwise NOT', () => {
|
|
638
|
+
const spend = makeSpend([
|
|
639
|
+
pushChunk([0xff]),
|
|
640
|
+
{ op: OP.OP_INVERT },
|
|
641
|
+
pushChunk([0x00]),
|
|
642
|
+
{ op: OP.OP_EQUAL }
|
|
643
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
644
|
+
expect(spend.validate()).toBe(true)
|
|
645
|
+
})
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
// ---------------------------------------------------------------------------
|
|
649
|
+
// OP_2MUL / OP_2DIV (lines 775-776)
|
|
650
|
+
// ---------------------------------------------------------------------------
|
|
651
|
+
describe('Spend – OP_2MUL / OP_2DIV', () => {
|
|
652
|
+
it('OP_2MUL doubles the top stack value', () => {
|
|
653
|
+
const spend = makeSpend([
|
|
654
|
+
pushChunk(scriptNum(7)),
|
|
655
|
+
{ op: OP.OP_2MUL },
|
|
656
|
+
pushChunk(scriptNum(14)),
|
|
657
|
+
{ op: OP.OP_NUMEQUAL }
|
|
658
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
659
|
+
expect(spend.validate()).toBe(true)
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('OP_2DIV halves the top stack value', () => {
|
|
663
|
+
const spend = makeSpend([
|
|
664
|
+
pushChunk(scriptNum(8)),
|
|
665
|
+
{ op: OP.OP_2DIV },
|
|
666
|
+
pushChunk(scriptNum(4)),
|
|
667
|
+
{ op: OP.OP_NUMEQUAL }
|
|
668
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
669
|
+
expect(spend.validate()).toBe(true)
|
|
670
|
+
})
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
// OP_CHECKMULTISIG error paths (lines 902, 908, 916, 922, 929)
|
|
675
|
+
// ---------------------------------------------------------------------------
|
|
676
|
+
describe('Spend – OP_CHECKMULTISIG error paths', () => {
|
|
677
|
+
it('throws when stack is empty (no nKeys item)', () => {
|
|
678
|
+
const spend = makeLocking([{ op: OP.OP_CHECKMULTISIG }])
|
|
679
|
+
expect(() => spend.validate()).toThrow('requires at least 1 item')
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
it('throws when nKeys is negative', () => {
|
|
683
|
+
const spend = makeLocking([
|
|
684
|
+
pushChunk(scriptNum(-1)),
|
|
685
|
+
{ op: OP.OP_CHECKMULTISIG }
|
|
686
|
+
])
|
|
687
|
+
expect(() => spend.validate()).toThrow('key count between 0')
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
it('throws when stack is too small for the declared keys', () => {
|
|
691
|
+
// nKeys=2 but only the nKeys element and 1 key are available
|
|
692
|
+
const spend = makeLocking([
|
|
693
|
+
pushChunk([0x01]), // one dummy key
|
|
694
|
+
pushChunk(scriptNum(2)), // nKeys=2
|
|
695
|
+
{ op: OP.OP_CHECKMULTISIG }
|
|
696
|
+
])
|
|
697
|
+
expect(() => spend.validate()).toThrow('stack too small for nKeys and keys')
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
it('throws when nSigs > nKeys', () => {
|
|
701
|
+
const dummyKey = [0x02, ...new Array(32).fill(0x00)] // 33-byte invalid key (will fail later)
|
|
702
|
+
// nSigs=3 but nKeys=1
|
|
703
|
+
const spend = makeLocking([
|
|
704
|
+
pushChunk([0x00]), // dummy
|
|
705
|
+
pushChunk(scriptNum(3)), // nSigs
|
|
706
|
+
pushChunk(dummyKey), // key 1
|
|
707
|
+
pushChunk(scriptNum(1)), // nKeys=1
|
|
708
|
+
{ op: OP.OP_CHECKMULTISIG }
|
|
709
|
+
])
|
|
710
|
+
expect(() => spend.validate()).toThrow('number of signatures to be no greater than the number of keys')
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('throws when stack is too small for the declared sigs', () => {
|
|
714
|
+
const dummyKey = [0x02, ...new Array(32).fill(0x00)]
|
|
715
|
+
// nKeys=1, nSigs=1 but no sig or dummy on stack
|
|
716
|
+
const spend = makeLocking([
|
|
717
|
+
pushChunk(dummyKey), // key 1
|
|
718
|
+
pushChunk(scriptNum(1)), // nKeys=1
|
|
719
|
+
{ op: OP.OP_CHECKMULTISIG }
|
|
720
|
+
// nSigs would be next but stack ran out
|
|
721
|
+
])
|
|
722
|
+
expect(() => spend.validate()).toThrow()
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
it('throws when non-empty dummy is present in strict mode', () => {
|
|
726
|
+
const privKey = PrivateKey.fromRandom()
|
|
727
|
+
const pubKey = PublicKey.fromPrivateKey(privKey)
|
|
728
|
+
// 0 of 0 multisig with non-empty dummy → NULLDUMMY violation
|
|
729
|
+
const spend = makeSpend([
|
|
730
|
+
pushChunk([0x01]), // non-empty dummy (violates SCRIPT_VERIFY_NULLDUMMY)
|
|
731
|
+
pushChunk(scriptNum(0)), // nSigs=0
|
|
732
|
+
pushChunk(scriptNum(0)), // nKeys=0
|
|
733
|
+
{ op: OP.OP_CHECKMULTISIG }
|
|
734
|
+
])
|
|
735
|
+
expect(() => spend.validate()).toThrow('dummy')
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
it('succeeds for 0-of-0 multisig with empty dummy', () => {
|
|
739
|
+
const spend = makeSpend([
|
|
740
|
+
{ op: OP.OP_0 }, // empty dummy
|
|
741
|
+
{ op: OP.OP_0 }, // nSigs=0
|
|
742
|
+
{ op: OP.OP_0 }, // nKeys=0
|
|
743
|
+
{ op: OP.OP_CHECKMULTISIG }
|
|
744
|
+
])
|
|
745
|
+
expect(spend.validate()).toBe(true)
|
|
746
|
+
})
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
// ---------------------------------------------------------------------------
|
|
750
|
+
// OP_NUM2BIN (lines 1029-1068)
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
describe('Spend – OP_NUM2BIN', () => {
|
|
753
|
+
it('throws when the requested size is too small for the value', () => {
|
|
754
|
+
// 256 needs 2 bytes; requesting 1 byte → error
|
|
755
|
+
const spend = makeSpend([
|
|
756
|
+
pushChunk(scriptNum(256)),
|
|
757
|
+
pushChunk(scriptNum(1)), // size = 1 → too small
|
|
758
|
+
{ op: OP.OP_NUM2BIN },
|
|
759
|
+
{ op: OP.OP_1 }
|
|
760
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
761
|
+
expect(() => spend.validate()).toThrow('OP_NUM2BIN requires that the size')
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
it('pads a positive number to the requested size', () => {
|
|
765
|
+
// value=5, size=4 → [0x05, 0x00, 0x00, 0x00]
|
|
766
|
+
const spend = makeSpend([
|
|
767
|
+
pushChunk(scriptNum(5)),
|
|
768
|
+
pushChunk(scriptNum(4)),
|
|
769
|
+
{ op: OP.OP_NUM2BIN },
|
|
770
|
+
pushChunk([0x05, 0x00, 0x00, 0x00]),
|
|
771
|
+
{ op: OP.OP_EQUAL }
|
|
772
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
773
|
+
expect(spend.validate()).toBe(true)
|
|
774
|
+
})
|
|
775
|
+
|
|
776
|
+
it('pads a negative number to the requested size (sign bit preserved)', () => {
|
|
777
|
+
// value=-5 (scriptnum: [0x85]), size=4 → [0x05, 0x00, 0x00, 0x80]
|
|
778
|
+
const spend = makeSpend([
|
|
779
|
+
pushChunk(scriptNum(-5)),
|
|
780
|
+
pushChunk(scriptNum(4)),
|
|
781
|
+
{ op: OP.OP_NUM2BIN },
|
|
782
|
+
pushChunk([0x05, 0x00, 0x00, 0x80]),
|
|
783
|
+
{ op: OP.OP_EQUAL }
|
|
784
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
785
|
+
expect(spend.validate()).toBe(true)
|
|
786
|
+
})
|
|
787
|
+
|
|
788
|
+
it('does not pad when rawnum length equals requested size', () => {
|
|
789
|
+
// value=256 (scriptnum [0x00, 0x01]), size=2 → [0x00, 0x01]
|
|
790
|
+
const spend = makeSpend([
|
|
791
|
+
pushChunk(scriptNum(256)),
|
|
792
|
+
pushChunk(scriptNum(2)),
|
|
793
|
+
{ op: OP.OP_NUM2BIN },
|
|
794
|
+
pushChunk([0x00, 0x01]),
|
|
795
|
+
{ op: OP.OP_EQUAL }
|
|
796
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
797
|
+
expect(spend.validate()).toBe(true)
|
|
798
|
+
})
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
// ---------------------------------------------------------------------------
|
|
802
|
+
// OP_BIN2NUM (line 1070-1078)
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
describe('Spend – OP_BIN2NUM', () => {
|
|
805
|
+
it('converts binary to minimal scriptnum', () => {
|
|
806
|
+
// [0x05, 0x00, 0x00, 0x00] → 5
|
|
807
|
+
const spend = makeSpend([
|
|
808
|
+
pushChunk([0x05, 0x00, 0x00, 0x00]),
|
|
809
|
+
{ op: OP.OP_BIN2NUM },
|
|
810
|
+
pushChunk(scriptNum(5)),
|
|
811
|
+
{ op: OP.OP_NUMEQUAL }
|
|
812
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
813
|
+
expect(spend.validate()).toBe(true)
|
|
814
|
+
})
|
|
815
|
+
|
|
816
|
+
it('converts padded negative binary to minimal scriptnum', () => {
|
|
817
|
+
// [0x05, 0x00, 0x00, 0x80] → -5
|
|
818
|
+
const spend = makeSpend([
|
|
819
|
+
pushChunk([0x05, 0x00, 0x00, 0x80]),
|
|
820
|
+
{ op: OP.OP_BIN2NUM },
|
|
821
|
+
pushChunk(scriptNum(-5)),
|
|
822
|
+
{ op: OP.OP_NUMEQUAL }
|
|
823
|
+
], [], { isRelaxed: true, memoryLimit: 100000 })
|
|
824
|
+
expect(spend.validate()).toBe(true)
|
|
825
|
+
})
|
|
826
|
+
})
|
|
827
|
+
|
|
828
|
+
// ---------------------------------------------------------------------------
|
|
829
|
+
// Default opcode path – invalid opcode (line 1082)
|
|
830
|
+
// ---------------------------------------------------------------------------
|
|
831
|
+
describe('Spend – default opcode (invalid)', () => {
|
|
832
|
+
it('throws for an invalid opcode value', () => {
|
|
833
|
+
// 0xff is not a valid opcode in BSV
|
|
834
|
+
const spend = makeLocking([{ op: 0xff }])
|
|
835
|
+
expect(() => spend.validate()).toThrow('Invalid opcode')
|
|
836
|
+
})
|
|
837
|
+
})
|