@bsv/sdk 1.1.22 → 1.1.24

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.
Files changed (78) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/compat/BSM.js +10 -3
  3. package/dist/cjs/src/compat/BSM.js.map +1 -1
  4. package/dist/cjs/src/primitives/Curve.js +7 -7
  5. package/dist/cjs/src/primitives/Curve.js.map +1 -1
  6. package/dist/cjs/src/primitives/ECDSA.js +394 -71
  7. package/dist/cjs/src/primitives/ECDSA.js.map +1 -1
  8. package/dist/cjs/src/primitives/Point.js +103 -23
  9. package/dist/cjs/src/primitives/Point.js.map +1 -1
  10. package/dist/cjs/src/primitives/TransactionSignature.js +4 -3
  11. package/dist/cjs/src/primitives/TransactionSignature.js.map +1 -1
  12. package/dist/cjs/src/primitives/utils.js +53 -16
  13. package/dist/cjs/src/primitives/utils.js.map +1 -1
  14. package/dist/cjs/src/script/Spend.js +6 -43
  15. package/dist/cjs/src/script/Spend.js.map +1 -1
  16. package/dist/cjs/src/script/templates/P2PKH.js +2 -2
  17. package/dist/cjs/src/script/templates/P2PKH.js.map +1 -1
  18. package/dist/cjs/src/script/templates/RPuzzle.js +2 -2
  19. package/dist/cjs/src/script/templates/RPuzzle.js.map +1 -1
  20. package/dist/cjs/src/transaction/Transaction.js +79 -65
  21. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  22. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  23. package/dist/esm/src/compat/BSM.js +10 -3
  24. package/dist/esm/src/compat/BSM.js.map +1 -1
  25. package/dist/esm/src/primitives/Curve.js +7 -7
  26. package/dist/esm/src/primitives/Curve.js.map +1 -1
  27. package/dist/esm/src/primitives/ECDSA.js +394 -71
  28. package/dist/esm/src/primitives/ECDSA.js.map +1 -1
  29. package/dist/esm/src/primitives/Point.js +103 -23
  30. package/dist/esm/src/primitives/Point.js.map +1 -1
  31. package/dist/esm/src/primitives/TransactionSignature.js +4 -3
  32. package/dist/esm/src/primitives/TransactionSignature.js.map +1 -1
  33. package/dist/esm/src/primitives/utils.js +51 -15
  34. package/dist/esm/src/primitives/utils.js.map +1 -1
  35. package/dist/esm/src/script/Spend.js +5 -42
  36. package/dist/esm/src/script/Spend.js.map +1 -1
  37. package/dist/esm/src/script/templates/P2PKH.js +2 -2
  38. package/dist/esm/src/script/templates/P2PKH.js.map +1 -1
  39. package/dist/esm/src/script/templates/RPuzzle.js +2 -2
  40. package/dist/esm/src/script/templates/RPuzzle.js.map +1 -1
  41. package/dist/esm/src/transaction/Transaction.js +79 -65
  42. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  43. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  44. package/dist/types/src/compat/BSM.d.ts +3 -2
  45. package/dist/types/src/compat/BSM.d.ts.map +1 -1
  46. package/dist/types/src/primitives/ECDSA.d.ts.map +1 -1
  47. package/dist/types/src/primitives/Point.d.ts +5 -0
  48. package/dist/types/src/primitives/Point.d.ts.map +1 -1
  49. package/dist/types/src/primitives/TransactionSignature.d.ts.map +1 -1
  50. package/dist/types/src/primitives/utils.d.ts +1 -0
  51. package/dist/types/src/primitives/utils.d.ts.map +1 -1
  52. package/dist/types/src/script/Spend.d.ts.map +1 -1
  53. package/dist/types/src/script/templates/P2PKH.d.ts +1 -1
  54. package/dist/types/src/script/templates/P2PKH.d.ts.map +1 -1
  55. package/dist/types/src/script/templates/RPuzzle.d.ts +1 -1
  56. package/dist/types/src/script/templates/RPuzzle.d.ts.map +1 -1
  57. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  58. package/dist/types/src/transaction/TransactionInput.d.ts +1 -1
  59. package/dist/types/src/transaction/TransactionInput.d.ts.map +1 -1
  60. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  61. package/docs/compat.md +10 -4
  62. package/docs/primitives.md +48 -12
  63. package/docs/script.md +4 -4
  64. package/docs/transaction.md +2 -1
  65. package/package.json +1 -1
  66. package/src/compat/BSM.ts +10 -3
  67. package/src/compat/__tests/BSM.test.ts +7 -2
  68. package/src/primitives/Curve.ts +7 -7
  69. package/src/primitives/ECDSA.ts +485 -75
  70. package/src/primitives/Point.ts +110 -25
  71. package/src/primitives/TransactionSignature.ts +4 -3
  72. package/src/primitives/utils.ts +58 -11
  73. package/src/script/Spend.ts +5 -48
  74. package/src/script/templates/P2PKH.ts +7 -7
  75. package/src/script/templates/RPuzzle.ts +8 -8
  76. package/src/transaction/Transaction.ts +93 -68
  77. package/src/transaction/TransactionInput.ts +1 -1
  78. package/src/transaction/__tests/Transaction.benchmarks.test.ts +222 -0
@@ -17,6 +17,10 @@ import ReductionContext from './ReductionContext.js'
17
17
  * @property inf - Flag to record if the point is at infinity in the Elliptic Curve.
18
18
  */
19
19
  export default class Point extends BasePoint {
20
+ private static readonly red: any = new ReductionContext('k256')
21
+ private static readonly a: BigNumber = new BigNumber(0).toRed(Point.red)
22
+ private static readonly b: BigNumber = new BigNumber(7).toRed(Point.red)
23
+ private static readonly zero: BigNumber = new BigNumber(0).toRed(Point.red)
20
24
  x: BigNumber | null
21
25
  y: BigNumber | null
22
26
  inf: boolean
@@ -59,7 +63,7 @@ export default class Point extends BasePoint {
59
63
 
60
64
  return res
61
65
  } else if ((bytes[0] === 0x02 || bytes[0] === 0x03) &&
62
- bytes.length - 1 === len) {
66
+ bytes.length - 1 === len) {
63
67
  return Point.fromX(bytes.slice(1, 1 + len), bytes[0] === 0x03)
64
68
  }
65
69
  throw new Error('Unknown point format')
@@ -87,6 +91,13 @@ export default class Point extends BasePoint {
87
91
  return Point.fromDER(bytes)
88
92
  }
89
93
 
94
+ static redSqrtOptimized (y2: BigNumber): BigNumber {
95
+ const red = Point.red
96
+ const p = red.m // The modulus
97
+ const exponent = p.addn(1).iushrn(2) // (p + 1) / 4
98
+ return y2.redPow(exponent)
99
+ }
100
+
90
101
  /**
91
102
  * Generates a point from an x coordinate and a boolean indicating whether the corresponding
92
103
  * y coordinate is odd.
@@ -102,34 +113,108 @@ export default class Point extends BasePoint {
102
113
  * const xCoordinate = new BigNumber('10');
103
114
  * const point = Point.fromX(xCoordinate, true);
104
115
  */
105
-
106
116
  static fromX (x: BigNumber | number | number[] | string, odd: boolean): Point {
107
- const red = new ReductionContext('k256')
108
- const a = new BigNumber(0).toRed(red)
109
- const b = new BigNumber(7).toRed(red)
110
- const zero = new BigNumber(0).toRed(red)
111
- if (!BigNumber.isBN(x)) {
112
- x = new BigNumber(x as number, 16)
113
- }
114
- x = x as BigNumber
115
- if (x.red == null) {
116
- x = x.toRed(red)
117
- }
117
+ if (typeof BigInt === 'function') {
118
+ function mod (a: bigint, n: bigint): bigint {
119
+ return ((a % n) + n) % n
120
+ }
121
+ function modPow (base: bigint, exponent: bigint, modulus: bigint): bigint {
122
+ let result = BigInt(1)
123
+ base = mod(base, modulus)
124
+ while (exponent > BigInt(0)) {
125
+ if ((exponent & BigInt(1)) === BigInt(1)) {
126
+ result = mod(result * base, modulus)
127
+ }
128
+ exponent >>= BigInt(1)
129
+ base = mod(base * base, modulus)
130
+ }
131
+ return result
132
+ }
133
+ function sqrtMod (a: bigint, p: bigint): bigint | null {
134
+ const exponent = (p + BigInt(1)) >> BigInt(2) // Precomputed exponent
135
+ const sqrtCandidate = modPow(a, exponent, p)
136
+ if (mod(sqrtCandidate * sqrtCandidate, p) === mod(a, p)) {
137
+ return sqrtCandidate
138
+ } else {
139
+ // No square root exists
140
+ return null
141
+ }
142
+ }
118
143
 
119
- const y2 = x.redSqr().redMul(x).redIAdd(x.redMul(a)).redIAdd(b)
120
- let y = y2.redSqrt()
121
- if (y.redSqr().redSub(y2).cmp(zero) !== 0) {
122
- throw new Error('invalid point')
123
- }
144
+ // Curve parameters for secp256k1
145
+ const p = BigInt(
146
+ '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F'
147
+ )
148
+ const a = BigInt(0)
149
+ const b = BigInt(7)
150
+
151
+ // Convert x to BigInt
152
+ let xBigInt: bigint
153
+ if (x instanceof BigNumber) {
154
+ xBigInt = BigInt('0x' + x.toString(16))
155
+ } else if (typeof x === 'string') {
156
+ xBigInt = BigInt('0x' + x)
157
+ } else if (Array.isArray(x)) {
158
+ xBigInt = BigInt(
159
+ '0x' +
160
+ Buffer.from(x).toString('hex').padStart(64, '0')
161
+ )
162
+ } else if (typeof x === 'number') {
163
+ xBigInt = BigInt(x)
164
+ } else {
165
+ throw new Error('Invalid x-coordinate type')
166
+ }
124
167
 
125
- // XXX Is there any way to tell if the number is odd without converting it
126
- // to non-red form?
127
- const isOdd = y.fromRed().isOdd()
128
- if ((odd && !isOdd) || (!odd && isOdd)) {
129
- y = y.redNeg()
130
- }
168
+ // Ensure x is within field range
169
+ xBigInt = mod(xBigInt, p)
131
170
 
132
- return new Point(x, y)
171
+ // Compute y^2 = x^3 + a x + b mod p
172
+ const y2 = mod(modPow(xBigInt, BigInt(3), p) + b, p)
173
+
174
+ // Compute modular square root y = sqrt(y2) mod p
175
+ let y = sqrtMod(y2, p)
176
+
177
+ if (y === null) {
178
+ throw new Error('Invalid point')
179
+ }
180
+
181
+ // Adjust y to match the oddness
182
+ const isYOdd = (y % BigInt(2)) === BigInt(1)
183
+ if ((odd && !isYOdd) || (!odd && isYOdd)) {
184
+ y = p - y
185
+ }
186
+
187
+ // Convert x and y to BigNumber
188
+ const xBN = new BigNumber(xBigInt.toString(16), 16)
189
+ const yBN = new BigNumber(y.toString(16), 16)
190
+ return new Point(xBN, yBN)
191
+ } else {
192
+ const red = new ReductionContext('k256')
193
+ const a = new BigNumber(0).toRed(red)
194
+ const b = new BigNumber(7).toRed(red)
195
+ const zero = new BigNumber(0).toRed(red)
196
+ if (!BigNumber.isBN(x)) {
197
+ x = new BigNumber(x as number, 16)
198
+ }
199
+ x = x as BigNumber
200
+ if (x.red == null) {
201
+ x = x.toRed(red)
202
+ }
203
+
204
+ const y2 = x.redSqr().redMul(x).redIAdd(x.redMul(a)).redIAdd(b)
205
+ let y = y2.redSqrt()
206
+ if (y.redSqr().redSub(y2).cmp(zero) !== 0) {
207
+ throw new Error('invalid point')
208
+ }
209
+
210
+ // XXX Is there any way to tell if the number is odd without converting it
211
+ // to non-red form?
212
+ const isOdd = y.fromRed().isOdd()
213
+ if ((odd && !isOdd) || (!odd && isOdd)) {
214
+ y = y.redNeg()
215
+ }
216
+ return new Point(x, y)
217
+ }
133
218
  }
134
219
 
135
220
  /**
@@ -40,7 +40,7 @@ export default class TransactionSignature extends Signature {
40
40
  const writer = new Writer()
41
41
  for (const input of inputs) {
42
42
  if (typeof input.sourceTXID === 'undefined') {
43
- writer.writeReverse(input.sourceTransaction.id())
43
+ writer.write(input.sourceTransaction.hash() as number[])
44
44
  } else {
45
45
  writer.writeReverse(toArray(input.sourceTXID, 'hex'))
46
46
  }
@@ -122,8 +122,9 @@ export default class TransactionSignature extends Signature {
122
122
  writer.writeUInt32LE(params.sourceOutputIndex)
123
123
 
124
124
  // scriptCode of the input (serialized as scripts inside CTxOuts)
125
- writer.writeVarIntNum(params.subscript.toBinary().length)
126
- writer.write(params.subscript.toBinary())
125
+ const subscriptBin = params.subscript.toBinary()
126
+ writer.writeVarIntNum(subscriptBin.length)
127
+ writer.write(subscriptBin)
127
128
 
128
129
  // value of the output spent by this input (8-byte little endian)
129
130
  writer.writeUInt64LE(params.sourceSatoshis)
@@ -320,9 +320,13 @@ export class Writer {
320
320
  }
321
321
 
322
322
  toArray (): number[] {
323
- let ret = []
324
- for (const x of this.bufs) {
325
- if (x.length < 65536) { ret.push(...x) } else { ret = ret.concat(x) }
323
+ const totalLength = this.getLength()
324
+ const ret = new Array(totalLength)
325
+ let offset = 0
326
+ for (const buf of this.bufs) {
327
+ for (let i = 0; i < buf.length; i++) {
328
+ ret[offset++] = buf[i]
329
+ }
326
330
  }
327
331
  return ret
328
332
  }
@@ -515,18 +519,18 @@ export class Reader {
515
519
  }
516
520
 
517
521
  public read (len = this.bin.length): number[] {
518
- const bin = this.bin.slice(this.pos, this.pos + len)
519
- this.pos = this.pos + len
520
- return bin
522
+ const start = this.pos
523
+ const end = this.pos + len
524
+ this.pos = end
525
+ return this.bin.slice(start, end)
521
526
  }
522
527
 
523
528
  public readReverse (len = this.bin.length): number[] {
524
- const bin = this.bin.slice(this.pos, this.pos + len)
525
- this.pos = this.pos + len
526
- const buf2 = new Array(bin.length)
527
- for (let i = 0; i < buf2.length; i++) {
528
- buf2[i] = bin[bin.length - 1 - i]
529
+ const buf2 = new Array(len)
530
+ for (let i = 0; i < len; i++) {
531
+ buf2[i] = this.bin[this.pos + len - 1 - i]
529
532
  }
533
+ this.pos += len
530
534
  return buf2
531
535
  }
532
536
 
@@ -662,3 +666,46 @@ export class Reader {
662
666
  }
663
667
  }
664
668
  }
669
+
670
+ export const minimallyEncode = (buf: number[]): number[] => {
671
+ if (buf.length === 0) {
672
+ return buf
673
+ }
674
+
675
+ // If the last byte is not 0x00 or 0x80, we are minimally encoded.
676
+ const last = buf[buf.length - 1]
677
+ if ((last & 0x7f) !== 0) {
678
+ return buf
679
+ }
680
+
681
+ // If the script is one byte long, then we have a zero, which encodes as an
682
+ // empty array.
683
+ if (buf.length === 1) {
684
+ return []
685
+ }
686
+
687
+ // If the next byte has it sign bit set, then we are minimaly encoded.
688
+ if ((buf[buf.length - 2] & 0x80) !== 0) {
689
+ return buf
690
+ }
691
+
692
+ // We are not minimally encoded, we need to figure out how much to trim.
693
+ for (let i = buf.length - 1; i > 0; i--) {
694
+ // We found a non zero byte, time to encode.
695
+ if (buf[i - 1] !== 0) {
696
+ if ((buf[i - 1] & 0x80) !== 0) {
697
+ // We found a byte with it sign bit set so we need one more
698
+ // byte.
699
+ buf[i++] = last
700
+ } else {
701
+ // the sign bit is clear, we can use it.
702
+ buf[i - 1] |= last
703
+ }
704
+
705
+ return buf.slice(0, i)
706
+ }
707
+ }
708
+
709
+ // If we found the whole thing is zeros, then we have a zero.
710
+ return []
711
+ }
@@ -4,7 +4,7 @@ import Script from './Script.js'
4
4
  import BigNumber from '../primitives/BigNumber.js'
5
5
  import OP from './OP.js'
6
6
  import ScriptChunk from './ScriptChunk.js'
7
- import { toHex } from '../primitives/utils.js'
7
+ import { toHex, minimallyEncode } from '../primitives/utils.js'
8
8
  import * as Hash from '../primitives/Hash.js'
9
9
  import TransactionSignature from '../primitives/TransactionSignature.js'
10
10
  import PublicKey from '../primitives/PublicKey.js'
@@ -206,53 +206,10 @@ export default class Spend {
206
206
  return true
207
207
  }
208
208
 
209
- const minimallyEncode = (buf: number[]): number[] => {
210
- if (buf.length === 0) {
211
- return buf
212
- }
213
-
214
- // If the last byte is not 0x00 or 0x80, we are minimally encoded.
215
- const last = buf[buf.length - 1]
216
- if ((last & 0x7f) !== 0) {
217
- return buf
218
- }
219
-
220
- // If the script is one byte long, then we have a zero, which encodes as an
221
- // empty array.
222
- if (buf.length === 1) {
223
- return []
224
- }
225
-
226
- // If the next byte has it sign bit set, then we are minimaly encoded.
227
- if ((buf[buf.length - 2] & 0x80) !== 0) {
228
- return buf
229
- }
230
-
231
- // We are not minimally encoded, we need to figure out how much to trim.
232
- for (let i = buf.length - 1; i > 0; i--) {
233
- // We found a non zero byte, time to encode.
234
- if (buf[i - 1] !== 0) {
235
- if ((buf[i - 1] & 0x80) !== 0) {
236
- // We found a byte with it sign bit set so we need one more
237
- // byte.
238
- buf[i++] = last
239
- } else {
240
- // the sign bit is clear, we can use it.
241
- buf[i - 1] |= last
242
- }
243
-
244
- return buf.slice(0, i)
245
- }
246
- }
247
-
248
- // If we found the whole thing is zeros, then we have a zero.
249
- return []
250
- }
251
-
252
209
  const padDataToSize = (buf: number[], len: number): number[] => {
253
- let b = buf
210
+ const b = buf
254
211
  while (b.length < len) {
255
- b = [0x00, ...b]
212
+ b.unshift(0)
256
213
  }
257
214
  return b
258
215
  }
@@ -828,7 +785,7 @@ export default class Spend {
828
785
  shifted = bn1.ushrn(n)
829
786
  }
830
787
  const bufShifted = padDataToSize(
831
- [...shifted.toArray().slice(buf1.length * -1)],
788
+ shifted.toArray().slice(buf1.length * -1),
832
789
  buf1.length
833
790
  )
834
791
  this.stack.push(bufShifted)
@@ -1059,7 +1016,7 @@ export default class Spend {
1059
1016
 
1060
1017
  try {
1061
1018
  sig = TransactionSignature.fromChecksigFormat(bufSig)
1062
- pubkey = PublicKey.fromString(toHex(bufPubkey))
1019
+ pubkey = PublicKey.fromDER(bufPubkey)
1063
1020
  fSuccess = verifySignature(sig, pubkey, subscript)
1064
1021
  } catch (e) {
1065
1022
  // invalid sig or pubkey
@@ -21,7 +21,7 @@ export default class P2PKH implements ScriptTemplate {
21
21
  * @param {number[] | string} pubkeyhash or address - An array or address representing the public key hash.
22
22
  * @returns {LockingScript} - A P2PKH locking script.
23
23
  */
24
- lock (pubkeyhash: string | number[]): LockingScript {
24
+ lock(pubkeyhash: string | number[]): LockingScript {
25
25
  let data: number[]
26
26
  if (typeof pubkeyhash === 'string') {
27
27
  const hash = fromBase58Check(pubkeyhash)
@@ -54,16 +54,16 @@ export default class P2PKH implements ScriptTemplate {
54
54
  * @param {Script} lockingScript - Optional. The lockinScript. Otherwise the input.sourceTransaction is required.
55
55
  * @returns {Object} - An object containing the `sign` and `estimateLength` functions.
56
56
  */
57
- unlock (
57
+ unlock(
58
58
  privateKey: PrivateKey,
59
59
  signOutputs: 'all' | 'none' | 'single' = 'all',
60
60
  anyoneCanPay: boolean = false,
61
61
  sourceSatoshis?: number,
62
62
  lockingScript?: Script
63
63
  ): {
64
- sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>
65
- estimateLength: () => Promise<106>
66
- } {
64
+ sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>
65
+ estimateLength: () => Promise<108>
66
+ } {
67
67
  return {
68
68
  sign: async (tx: Transaction, inputIndex: number) => {
69
69
  let signatureScope = TransactionSignature.SIGHASH_FORKID
@@ -130,9 +130,9 @@ export default class P2PKH implements ScriptTemplate {
130
130
  ])
131
131
  },
132
132
  estimateLength: async () => {
133
- // public key (1+33) + signature (1+71)
133
+ // public key (1+33) + signature (1+73)
134
134
  // Note: We add 1 to each element's length because of the associated OP_PUSH
135
- return 106
135
+ return 108
136
136
  }
137
137
  }
138
138
  }
@@ -23,7 +23,7 @@ export default class RPuzzle implements ScriptTemplate {
23
23
  *
24
24
  * @param {'raw'|'SHA1'|'SHA256'|'HASH256'|'RIPEMD160'|'HASH160'} type Denotes the type of puzzle to create
25
25
  */
26
- constructor (type: 'raw' | 'SHA1' | 'SHA256' | 'HASH256' | 'RIPEMD160' | 'HASH160' = 'raw') {
26
+ constructor(type: 'raw' | 'SHA1' | 'SHA256' | 'HASH256' | 'RIPEMD160' | 'HASH160' = 'raw') {
27
27
  this.type = type
28
28
  }
29
29
 
@@ -33,7 +33,7 @@ export default class RPuzzle implements ScriptTemplate {
33
33
  * @param {number[]} value - An array representing the R value or its hash.
34
34
  * @returns {LockingScript} - An R puzzle locking script.
35
35
  */
36
- lock (value: number[]): LockingScript {
36
+ lock(value: number[]): LockingScript {
37
37
  const chunks: ScriptChunk[] = [
38
38
  { op: OP.OP_OVER },
39
39
  { op: OP.OP_3 },
@@ -70,15 +70,15 @@ export default class RPuzzle implements ScriptTemplate {
70
70
  * @param {boolean} anyoneCanPay - Flag indicating if the signature allows for other inputs to be added later.
71
71
  * @returns {Object} - An object containing the `sign` and `estimateLength` functions.
72
72
  */
73
- unlock (
73
+ unlock(
74
74
  k: BigNumber,
75
75
  privateKey: PrivateKey,
76
76
  signOutputs: 'all' | 'none' | 'single' = 'all',
77
77
  anyoneCanPay: boolean = false
78
78
  ): {
79
- sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>
80
- estimateLength: () => Promise<106>
81
- } {
79
+ sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>
80
+ estimateLength: () => Promise<108>
81
+ } {
82
82
  return {
83
83
  sign: async (tx: Transaction, inputIndex: number) => {
84
84
  if (typeof privateKey === 'undefined') {
@@ -131,9 +131,9 @@ export default class RPuzzle implements ScriptTemplate {
131
131
  ])
132
132
  },
133
133
  estimateLength: async () => {
134
- // public key (1+33) + signature (1+71)
134
+ // public key (1+33) + signature (1+73)
135
135
  // Note: We add 1 to each element's length because of the associated OP_PUSH
136
- return 106
136
+ return 108
137
137
  }
138
138
  }
139
139
  }
@@ -593,9 +593,8 @@ export default class Transaction {
593
593
  }
594
594
  if (enc === 'hex') {
595
595
  return toHex(hash)
596
- } else {
597
- return hash
598
596
  }
597
+ return hash
599
598
  }
600
599
 
601
600
  /**
@@ -635,84 +634,110 @@ export default class Transaction {
635
634
  *
636
635
  * @example tx.verify(new WhatsOnChain(), new SatoshisPerKilobyte(1))
637
636
  */
638
- async verify (chainTracker: ChainTracker | 'scripts only' = defaultChainTracker(), feeModel?: FeeModel): Promise<boolean> {
639
- // If the transaction has a valid merkle path, verification is complete.
640
- if (typeof this.merklePath === 'object') {
641
- if (chainTracker === 'scripts only') {
642
- return true
643
- } else {
644
- const proofValid = await this.merklePath.verify(
645
- this.id('hex'),
646
- chainTracker
647
- )
648
- // Note that if the proof is provided but not valid, the transaction could still be verified by proving all inputs (if they are available) and checking the associated spends.
649
- if (proofValid) {
650
- return true
651
- }
637
+ async verify (
638
+ chainTracker: ChainTracker | 'scripts only' = defaultChainTracker(),
639
+ feeModel?: FeeModel
640
+ ): Promise<boolean> {
641
+ const verifiedTxids = new Set<string>()
642
+ const txQueue: Transaction[] = [this]
643
+
644
+ while (txQueue.length > 0) {
645
+ const tx = txQueue.shift()
646
+ const txid = tx.id('hex')
647
+ if (verifiedTxids.has(txid)) {
648
+ continue
652
649
  }
653
- }
654
-
655
- if (typeof feeModel !== 'undefined') {
656
- const cpTx = Transaction.fromHexEF(this.toHexEF())
657
- delete cpTx.outputs[0].satoshis
658
- cpTx.outputs[0].change = true
659
- await cpTx.fee(feeModel)
660
- if (this.getFee() < cpTx.getFee()) throw new Error(`Verification failed because the transaction ${this.id('hex')} has an insufficient fee and has not been mined.`)
661
- }
662
650
 
663
- // Verify each input transaction and evaluate the spend events.
664
- // Also, keep a total of the input amounts for later.
665
- let inputTotal = 0
666
- for (let i = 0; i < this.inputs.length; i++) {
667
- const input = this.inputs[i]
668
- if (typeof input.sourceTransaction !== 'object') {
669
- throw new Error(`Verification failed because the input at index ${i} of transaction ${this.id('hex')} is missing an associated source transaction. This source transaction is required for transaction verification because there is no merkle proof for the transaction spending a UTXO it contains.`)
670
- }
671
- if (typeof input.unlockingScript !== 'object') {
672
- throw new Error(`Verification failed because the input at index ${i} of transaction ${this.id('hex')} is missing an associated unlocking script. This script is required for transaction verification because there is no merkle proof for the transaction spending the UTXO.`)
651
+ // If the transaction has a valid merkle path, verification is complete.
652
+ if (typeof tx.merklePath === 'object') {
653
+ if (chainTracker === 'scripts only') {
654
+ verifiedTxids.add(txid)
655
+ continue
656
+ } else {
657
+ const proofValid = await tx.merklePath.verify(
658
+ txid,
659
+ chainTracker
660
+ )
661
+ // If the proof is valid, no need to verify inputs.
662
+ if (proofValid) {
663
+ verifiedTxids.add(txid)
664
+ continue
665
+ }
666
+ }
673
667
  }
674
- const sourceOutput = input.sourceTransaction.outputs[input.sourceOutputIndex]
675
- inputTotal += sourceOutput.satoshis
676
- const inputVerified = await input.sourceTransaction.verify(chainTracker, feeModel)
677
- if (!inputVerified) {
678
- return false
668
+
669
+ // Verify fee if feeModel is provided
670
+ if (typeof feeModel !== 'undefined') {
671
+ const cpTx = Transaction.fromEF(tx.toEF())
672
+ delete cpTx.outputs[0].satoshis
673
+ cpTx.outputs[0].change = true
674
+ await cpTx.fee(feeModel)
675
+ if (tx.getFee() < cpTx.getFee()) {
676
+ throw new Error(`Verification failed because the transaction ${txid} has an insufficient fee and has not been mined.`)
677
+ }
679
678
  }
680
- const otherInputs = [...this.inputs]
681
- otherInputs.splice(i, 1)
682
- if (typeof input.sourceTXID === 'undefined') {
683
- input.sourceTXID = input.sourceTransaction.id('hex')
679
+
680
+ // Verify each input transaction and evaluate the spend events.
681
+ // Also, keep a total of the input amounts for later.
682
+ let inputTotal = 0
683
+ for (let i = 0; i < tx.inputs.length; i++) {
684
+ const input = tx.inputs[i]
685
+ if (typeof input.sourceTransaction !== 'object') {
686
+ throw new Error(`Verification failed because the input at index ${i} of transaction ${txid} is missing an associated source transaction. This source transaction is required for transaction verification because there is no merkle proof for the transaction spending a UTXO it contains.`)
687
+ }
688
+ if (typeof input.unlockingScript !== 'object') {
689
+ throw new Error(`Verification failed because the input at index ${i} of transaction ${txid} is missing an associated unlocking script. This script is required for transaction verification because there is no merkle proof for the transaction spending the UTXO.`)
690
+ }
691
+ const sourceOutput = input.sourceTransaction.outputs[input.sourceOutputIndex]
692
+ inputTotal += sourceOutput.satoshis
693
+
694
+ const sourceTxid = input.sourceTransaction.id('hex')
695
+ if (!verifiedTxids.has(sourceTxid)) {
696
+ txQueue.push(input.sourceTransaction)
697
+ }
698
+
699
+ const otherInputs = tx.inputs.filter((_, idx) => idx !== i)
700
+ if (typeof input.sourceTXID === 'undefined') {
701
+ input.sourceTXID = sourceTxid
702
+ }
703
+
704
+ const spend = new Spend({
705
+ sourceTXID: input.sourceTXID,
706
+ sourceOutputIndex: input.sourceOutputIndex,
707
+ lockingScript: sourceOutput.lockingScript,
708
+ sourceSatoshis: sourceOutput.satoshis,
709
+ transactionVersion: tx.version,
710
+ otherInputs,
711
+ unlockingScript: input.unlockingScript,
712
+ inputSequence: input.sequence,
713
+ inputIndex: i,
714
+ outputs: tx.outputs,
715
+ lockTime: tx.lockTime
716
+ })
717
+ const spendValid = spend.validate()
718
+
719
+ if (!spendValid) {
720
+ return false
721
+ }
684
722
  }
685
723
 
686
- const spend = new Spend({
687
- sourceTXID: input.sourceTXID,
688
- sourceOutputIndex: input.sourceOutputIndex,
689
- lockingScript: sourceOutput.lockingScript,
690
- sourceSatoshis: sourceOutput.satoshis,
691
- transactionVersion: this.version,
692
- otherInputs,
693
- unlockingScript: input.unlockingScript,
694
- inputSequence: input.sequence,
695
- inputIndex: i,
696
- outputs: this.outputs,
697
- lockTime: this.lockTime
698
- })
699
- const spendValid = spend.validate()
724
+ // Total the outputs to ensure they don't amount to more than the inputs
725
+ let outputTotal = 0
726
+ for (const out of tx.outputs) {
727
+ if (typeof out.satoshis !== 'number') {
728
+ throw new Error('Every output must have a defined amount during transaction verification.')
729
+ }
730
+ outputTotal += out.satoshis
731
+ }
700
732
 
701
- if (!spendValid) {
733
+ if (outputTotal > inputTotal) {
702
734
  return false
703
735
  }
704
- }
705
736
 
706
- // Total the outputs to ensure they don't amount to more than the inputs
707
- let outputTotal = 0
708
- for (const out of this.outputs) {
709
- if (typeof out.satoshis !== 'number') {
710
- throw new Error('Every output must have a defined amount during transaction verification.')
711
- }
712
- outputTotal += out.satoshis
737
+ verifiedTxids.add(txid)
713
738
  }
714
739
 
715
- return outputTotal <= inputTotal
740
+ return true
716
741
  }
717
742
 
718
743
  /**
@@ -59,5 +59,5 @@ export default interface TransactionInput {
59
59
  sign: (tx: Transaction, inputIndex: number) => Promise<UnlockingScript>
60
60
  estimateLength: (tx: Transaction, inputIndex: number) => Promise<number>
61
61
  }
62
- sequence: number
62
+ sequence?: number
63
63
  }