@bsv/sdk 1.8.12 → 1.9.0

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 (102) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/Peer.js +35 -16
  3. package/dist/cjs/src/auth/Peer.js.map +1 -1
  4. package/dist/cjs/src/overlay-tools/HostReputationTracker.js +216 -0
  5. package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -0
  6. package/dist/cjs/src/overlay-tools/LookupResolver.js +55 -3
  7. package/dist/cjs/src/overlay-tools/LookupResolver.js.map +1 -1
  8. package/dist/cjs/src/primitives/BigNumber.js +43 -31
  9. package/dist/cjs/src/primitives/BigNumber.js.map +1 -1
  10. package/dist/cjs/src/primitives/Hash.js +11 -5
  11. package/dist/cjs/src/primitives/Hash.js.map +1 -1
  12. package/dist/cjs/src/primitives/SymmetricKey.js +15 -6
  13. package/dist/cjs/src/primitives/SymmetricKey.js.map +1 -1
  14. package/dist/cjs/src/primitives/TransactionSignature.js +60 -18
  15. package/dist/cjs/src/primitives/TransactionSignature.js.map +1 -1
  16. package/dist/cjs/src/primitives/utils.js +74 -28
  17. package/dist/cjs/src/primitives/utils.js.map +1 -1
  18. package/dist/cjs/src/script/Script.js +217 -108
  19. package/dist/cjs/src/script/Script.js.map +1 -1
  20. package/dist/cjs/src/script/Spend.js +5 -2
  21. package/dist/cjs/src/script/Spend.js.map +1 -1
  22. package/dist/cjs/src/transaction/Beef.js +62 -7
  23. package/dist/cjs/src/transaction/Beef.js.map +1 -1
  24. package/dist/cjs/src/transaction/BeefTx.js +1 -1
  25. package/dist/cjs/src/transaction/BeefTx.js.map +1 -1
  26. package/dist/cjs/src/transaction/Transaction.js +67 -35
  27. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  28. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  29. package/dist/esm/src/auth/Peer.js +36 -16
  30. package/dist/esm/src/auth/Peer.js.map +1 -1
  31. package/dist/esm/src/overlay-tools/HostReputationTracker.js +213 -0
  32. package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -0
  33. package/dist/esm/src/overlay-tools/LookupResolver.js +56 -3
  34. package/dist/esm/src/overlay-tools/LookupResolver.js.map +1 -1
  35. package/dist/esm/src/primitives/BigNumber.js +43 -31
  36. package/dist/esm/src/primitives/BigNumber.js.map +1 -1
  37. package/dist/esm/src/primitives/Hash.js +11 -5
  38. package/dist/esm/src/primitives/Hash.js.map +1 -1
  39. package/dist/esm/src/primitives/SymmetricKey.js +15 -6
  40. package/dist/esm/src/primitives/SymmetricKey.js.map +1 -1
  41. package/dist/esm/src/primitives/TransactionSignature.js +60 -18
  42. package/dist/esm/src/primitives/TransactionSignature.js.map +1 -1
  43. package/dist/esm/src/primitives/utils.js +74 -28
  44. package/dist/esm/src/primitives/utils.js.map +1 -1
  45. package/dist/esm/src/script/Script.js +222 -110
  46. package/dist/esm/src/script/Script.js.map +1 -1
  47. package/dist/esm/src/script/Spend.js +6 -2
  48. package/dist/esm/src/script/Spend.js.map +1 -1
  49. package/dist/esm/src/transaction/Beef.js +64 -7
  50. package/dist/esm/src/transaction/Beef.js.map +1 -1
  51. package/dist/esm/src/transaction/BeefTx.js +1 -1
  52. package/dist/esm/src/transaction/BeefTx.js.map +1 -1
  53. package/dist/esm/src/transaction/Transaction.js +69 -35
  54. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  55. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  56. package/dist/types/src/auth/Peer.d.ts +4 -0
  57. package/dist/types/src/auth/Peer.d.ts.map +1 -1
  58. package/dist/types/src/overlay-tools/HostReputationTracker.d.ts +37 -0
  59. package/dist/types/src/overlay-tools/HostReputationTracker.d.ts.map +1 -0
  60. package/dist/types/src/overlay-tools/LookupResolver.d.ts +8 -0
  61. package/dist/types/src/overlay-tools/LookupResolver.d.ts.map +1 -1
  62. package/dist/types/src/primitives/BigNumber.d.ts.map +1 -1
  63. package/dist/types/src/primitives/Hash.d.ts +10 -10
  64. package/dist/types/src/primitives/Hash.d.ts.map +1 -1
  65. package/dist/types/src/primitives/SymmetricKey.d.ts.map +1 -1
  66. package/dist/types/src/primitives/TransactionSignature.d.ts +34 -13
  67. package/dist/types/src/primitives/TransactionSignature.d.ts.map +1 -1
  68. package/dist/types/src/primitives/utils.d.ts +6 -8
  69. package/dist/types/src/primitives/utils.d.ts.map +1 -1
  70. package/dist/types/src/script/Script.d.ts +18 -9
  71. package/dist/types/src/script/Script.d.ts.map +1 -1
  72. package/dist/types/src/script/Spend.d.ts +1 -0
  73. package/dist/types/src/script/Spend.d.ts.map +1 -1
  74. package/dist/types/src/transaction/Beef.d.ts +9 -0
  75. package/dist/types/src/transaction/Beef.d.ts.map +1 -1
  76. package/dist/types/src/transaction/Transaction.d.ts +7 -0
  77. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  78. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  79. package/dist/umd/bundle.js +3 -3
  80. package/dist/umd/bundle.js.map +1 -1
  81. package/docs/reference/overlay-tools.md +58 -0
  82. package/docs/reference/primitives.md +120 -37
  83. package/docs/reference/script.md +11 -7
  84. package/docs/reference/transaction.md +2 -0
  85. package/docs/tutorials/advanced-transaction.md +2 -2
  86. package/docs/tutorials/first-transaction.md +1 -1
  87. package/docs/tutorials/transaction-types.md +1 -1
  88. package/package.json +1 -1
  89. package/src/auth/Peer.ts +44 -18
  90. package/src/overlay-tools/HostReputationTracker.ts +232 -0
  91. package/src/overlay-tools/LookupResolver.ts +73 -4
  92. package/src/overlay-tools/__tests/LookupResolver.test.ts +120 -0
  93. package/src/primitives/BigNumber.ts +44 -23
  94. package/src/primitives/Hash.ts +41 -17
  95. package/src/primitives/SymmetricKey.ts +15 -6
  96. package/src/primitives/TransactionSignature.ts +77 -31
  97. package/src/primitives/utils.ts +80 -30
  98. package/src/script/Script.ts +238 -104
  99. package/src/script/Spend.ts +7 -3
  100. package/src/transaction/Beef.ts +74 -7
  101. package/src/transaction/BeefTx.ts +1 -1
  102. package/src/transaction/Transaction.ts +77 -34
@@ -1,6 +1,6 @@
1
1
  import ScriptChunk from './ScriptChunk.js'
2
2
  import OP from './OP.js'
3
- import { encode, toHex, Reader, Writer, toArray } from '../primitives/utils.js'
3
+ import { encode, toHex, toArray } from '../primitives/utils.js'
4
4
  import BigNumber from '../primitives/BigNumber.js'
5
5
 
6
6
  /**
@@ -10,8 +10,14 @@ import BigNumber from '../primitives/BigNumber.js'
10
10
  *
11
11
  * @property {ScriptChunk[]} chunks - An array of script chunks that make up the script.
12
12
  */
13
+ const BufferCtor =
14
+ typeof globalThis !== 'undefined' ? (globalThis as any).Buffer : undefined
15
+
13
16
  export default class Script {
14
- chunks: ScriptChunk[]
17
+ private _chunks: ScriptChunk[]
18
+ private parsed: boolean
19
+ private rawBytesCache?: Uint8Array
20
+ private hexCache?: string
15
21
 
16
22
  /**
17
23
  * @method fromASM
@@ -110,7 +116,9 @@ export default class Script {
110
116
  if (!/^[0-9a-fA-F]+$/.test(hex)) {
111
117
  throw new Error('Some elements in this string are not hex encoded.')
112
118
  }
113
- return Script.fromBinary(toArray(hex, 'hex'))
119
+ const bin = toArray(hex, 'hex')
120
+ const rawBytes = Uint8Array.from(bin)
121
+ return new Script([], rawBytes, hex.toLowerCase(), false)
114
122
  }
115
123
 
116
124
  /**
@@ -122,89 +130,44 @@ export default class Script {
122
130
  * const script = Script.fromBinary([0x76, 0xa9, ...])
123
131
  */
124
132
  static fromBinary (bin: number[]): Script {
125
- bin = [...bin]
126
- const chunks: ScriptChunk[] = []
127
-
128
- let inConditionalBlock: number = 0
129
-
130
- const br = new Reader(bin)
131
- while (!br.eof()) {
132
- const op = br.readUInt8()
133
-
134
- // if OP_RETURN and not in a conditional block, do not parse the rest of the data,
135
- // rather just return the last chunk as data without prefixing with data length.
136
- if (op === OP.OP_RETURN && inConditionalBlock === 0) {
137
- chunks.push({
138
- op,
139
- data: br.read()
140
- })
141
- break
142
- }
143
-
144
- if (op === OP.OP_IF || op === OP.OP_NOTIF || op === OP.OP_VERIF || op === OP.OP_VERNOTIF) {
145
- inConditionalBlock++
146
- } else if (op === OP.OP_ENDIF) {
147
- inConditionalBlock--
148
- }
149
-
150
- let len = 0
151
- // eslint-disable-next-line @typescript-eslint/no-shadow
152
- let data: number[] = []
153
- if (op > 0 && op < OP.OP_PUSHDATA1) {
154
- len = op
155
- chunks.push({
156
- data: br.read(len),
157
- op
158
- })
159
- } else if (op === OP.OP_PUSHDATA1) {
160
- try {
161
- len = br.readUInt8()
162
- data = br.read(len)
163
- } catch {
164
- br.read()
165
- }
166
- chunks.push({
167
- data,
168
- op
169
- })
170
- } else if (op === OP.OP_PUSHDATA2) {
171
- try {
172
- len = br.readUInt16LE()
173
- data = br.read(len)
174
- } catch {
175
- br.read()
176
- }
177
- chunks.push({
178
- data,
179
- op
180
- })
181
- } else if (op === OP.OP_PUSHDATA4) {
182
- try {
183
- len = br.readUInt32LE()
184
- data = br.read(len)
185
- } catch {
186
- br.read()
187
- }
188
- chunks.push({
189
- data,
190
- op
191
- })
192
- } else {
193
- chunks.push({
194
- op
195
- })
196
- }
197
- }
198
- return new Script(chunks)
133
+ const rawBytes = Uint8Array.from(bin)
134
+ return new Script([], rawBytes, undefined, false)
199
135
  }
200
136
 
201
137
  /**
202
138
  * @constructor
203
139
  * Constructs a new Script object.
204
140
  * @param chunks=[] - An array of script chunks to directly initialize the script.
141
+ * @param rawBytesCache - Optional serialized bytes that can be reused instead of reserializing `chunks`.
142
+ * @param hexCache - Optional lowercase hex string that matches the serialized bytes, used to satisfy `toHex` quickly.
143
+ * @param parsed - When false the script defers parsing `rawBytesCache` until `chunks` is accessed; defaults to true.
205
144
  */
206
- constructor (chunks: ScriptChunk[] = []) {
207
- this.chunks = chunks
145
+ constructor (chunks: ScriptChunk[] = [], rawBytesCache?: Uint8Array, hexCache?: string, parsed: boolean = true) {
146
+ this._chunks = chunks
147
+ this.parsed = parsed
148
+ this.rawBytesCache = rawBytesCache
149
+ this.hexCache = hexCache
150
+ }
151
+
152
+ get chunks (): ScriptChunk[] {
153
+ this.ensureParsed()
154
+ return this._chunks
155
+ }
156
+
157
+ set chunks (value: ScriptChunk[]) {
158
+ this._chunks = value
159
+ this.parsed = true
160
+ this.invalidateSerializationCaches()
161
+ }
162
+
163
+ private ensureParsed (): void {
164
+ if (this.parsed) return
165
+ if (this.rawBytesCache != null) {
166
+ this._chunks = Script.parseChunks(this.rawBytesCache)
167
+ } else {
168
+ this._chunks = []
169
+ }
170
+ this.parsed = true
208
171
  }
209
172
 
210
173
  /**
@@ -228,7 +191,18 @@ export default class Script {
228
191
  * @returns The script in hexadecimal format.
229
192
  */
230
193
  toHex (): string {
231
- return encode(this.toBinary(), 'hex') as string
194
+ if (this.hexCache != null) {
195
+ return this.hexCache
196
+ }
197
+ if (this.rawBytesCache == null) {
198
+ this.rawBytesCache = this.serializeChunksToBytes()
199
+ }
200
+ const hex =
201
+ BufferCtor != null
202
+ ? BufferCtor.from(this.rawBytesCache).toString('hex')
203
+ : (encode(Array.from(this.rawBytesCache), 'hex') as string)
204
+ this.hexCache = hex
205
+ return hex
232
206
  }
233
207
 
234
208
  /**
@@ -237,32 +211,14 @@ export default class Script {
237
211
  * @returns The script in binary array format.
238
212
  */
239
213
  toBinary (): number[] {
240
- const writer = new Writer()
214
+ return Array.from(this.toUint8Array())
215
+ }
241
216
 
242
- for (let i = 0; i < this.chunks.length; i++) {
243
- const chunk = this.chunks[i]
244
- const op = chunk.op
245
- writer.writeUInt8(op)
246
- if (op === OP.OP_RETURN && chunk.data != null) { // special case for unformatted data
247
- writer.write(chunk.data)
248
- break
249
- } else if (chunk.data != null) {
250
- if (op < OP.OP_PUSHDATA1) {
251
- writer.write(chunk.data)
252
- } else if (op === OP.OP_PUSHDATA1) {
253
- writer.writeUInt8(chunk.data.length)
254
- writer.write(chunk.data)
255
- } else if (op === OP.OP_PUSHDATA2) {
256
- writer.writeUInt16LE(chunk.data.length)
257
- writer.write(chunk.data)
258
- } else if (op === OP.OP_PUSHDATA4) {
259
- writer.writeUInt32LE(chunk.data.length)
260
- writer.write(chunk.data)
261
- }
262
- }
217
+ toUint8Array (): Uint8Array {
218
+ if (this.rawBytesCache == null) {
219
+ this.rawBytesCache = this.serializeChunksToBytes()
263
220
  }
264
-
265
- return writer.toArray()
221
+ return this.rawBytesCache
266
222
  }
267
223
 
268
224
  /**
@@ -272,6 +228,7 @@ export default class Script {
272
228
  * @returns This script instance for chaining.
273
229
  */
274
230
  writeScript (script: Script): Script {
231
+ this.invalidateSerializationCaches()
275
232
  this.chunks = this.chunks.concat(script.chunks)
276
233
  return this
277
234
  }
@@ -283,6 +240,7 @@ export default class Script {
283
240
  * @returns This script instance for chaining.
284
241
  */
285
242
  writeOpCode (op: number): Script {
243
+ this.invalidateSerializationCaches()
286
244
  this.chunks.push({ op })
287
245
  return this
288
246
  }
@@ -295,6 +253,7 @@ export default class Script {
295
253
  * @returns This script instance for chaining.
296
254
  */
297
255
  setChunkOpCode (i: number, op: number): Script {
256
+ this.invalidateSerializationCaches()
298
257
  this.chunks[i] = { op }
299
258
  return this
300
259
  }
@@ -306,6 +265,7 @@ export default class Script {
306
265
  * @returns This script instance for chaining.
307
266
  */
308
267
  writeBn (bn: BigNumber): Script {
268
+ this.invalidateSerializationCaches()
309
269
  if (bn.cmpn(0) === OP.OP_0) {
310
270
  this.chunks.push({
311
271
  op: OP.OP_0
@@ -334,6 +294,7 @@ export default class Script {
334
294
  * @throws {Error} Throws an error if the data is too large to be pushed.
335
295
  */
336
296
  writeBin (bin: number[]): Script {
297
+ this.invalidateSerializationCaches()
337
298
  let op: number
338
299
  const data = bin.length > 0 ? bin : undefined
339
300
  if (bin.length > 0 && bin.length < OP.OP_PUSHDATA1) {
@@ -363,6 +324,7 @@ export default class Script {
363
324
  * @returns This script instance for chaining.
364
325
  */
365
326
  writeNumber (num: number): Script {
327
+ this.invalidateSerializationCaches()
366
328
  this.writeBn(new BigNumber(num))
367
329
  return this
368
330
  }
@@ -373,6 +335,7 @@ export default class Script {
373
335
  * @returns This script instance for chaining.
374
336
  */
375
337
  removeCodeseparators (): Script {
338
+ this.invalidateSerializationCaches()
376
339
  const chunks: ScriptChunk[] = []
377
340
  for (let i = 0; i < this.chunks.length; i++) {
378
341
  if (this.chunks[i].op !== OP.OP_CODESEPARATOR) {
@@ -391,6 +354,7 @@ export default class Script {
391
354
  * @returns This script instance for chaining.
392
355
  */
393
356
  findAndDelete (script: Script): Script {
357
+ this.invalidateSerializationCaches()
394
358
  const buf = script.toHex()
395
359
  for (let i = 0; i < this.chunks.length; i++) {
396
360
  const script2 = new Script([this.chunks[i]])
@@ -443,6 +407,176 @@ export default class Script {
443
407
  * @param chunk - The script chunk.
444
408
  * @returns The string representation of the chunk.
445
409
  */
410
+ private static computeSerializedLength (chunks: ScriptChunk[]): number {
411
+ let total = 0
412
+ for (const chunk of chunks) {
413
+ total += 1
414
+ if (chunk.data == null) continue
415
+ const len = chunk.data.length
416
+ if (chunk.op === OP.OP_RETURN) {
417
+ total += len
418
+ break
419
+ }
420
+ if (chunk.op < OP.OP_PUSHDATA1) {
421
+ total += len
422
+ } else if (chunk.op === OP.OP_PUSHDATA1) {
423
+ total += 1 + len
424
+ } else if (chunk.op === OP.OP_PUSHDATA2) {
425
+ total += 2 + len
426
+ } else if (chunk.op === OP.OP_PUSHDATA4) {
427
+ total += 4 + len
428
+ }
429
+ }
430
+ return total
431
+ }
432
+
433
+ private serializeChunksToBytes (): Uint8Array {
434
+ const chunks = this.chunks
435
+ const totalLength = Script.computeSerializedLength(chunks)
436
+ const bytes = new Uint8Array(totalLength)
437
+ let offset = 0
438
+
439
+ for (let i = 0; i < chunks.length; i++) {
440
+ const chunk = chunks[i]
441
+ bytes[offset++] = chunk.op
442
+ if (chunk.data == null) continue
443
+ if (chunk.op === OP.OP_RETURN) {
444
+ bytes.set(chunk.data, offset)
445
+ offset += chunk.data.length
446
+ break
447
+ }
448
+ offset = Script.writeChunkData(bytes, offset, chunk.op, chunk.data)
449
+ }
450
+
451
+ return bytes
452
+ }
453
+
454
+ private invalidateSerializationCaches (): void {
455
+ this.rawBytesCache = undefined
456
+ this.hexCache = undefined
457
+ }
458
+
459
+ private static writeChunkData (
460
+ target: Uint8Array,
461
+ offset: number,
462
+ op: number,
463
+ data: number[]
464
+ ): number {
465
+ const len = data.length
466
+ if (op < OP.OP_PUSHDATA1) {
467
+ target.set(data, offset)
468
+ return offset + len
469
+ } else if (op === OP.OP_PUSHDATA1) {
470
+ target[offset++] = len & 0xff
471
+ target.set(data, offset)
472
+ return offset + len
473
+ } else if (op === OP.OP_PUSHDATA2) {
474
+ target[offset++] = len & 0xff
475
+ target[offset++] = (len >> 8) & 0xff
476
+ target.set(data, offset)
477
+ return offset + len
478
+ } else if (op === OP.OP_PUSHDATA4) {
479
+ const size = len >>> 0
480
+ target[offset++] = size & 0xff
481
+ target[offset++] = (size >> 8) & 0xff
482
+ target[offset++] = (size >> 16) & 0xff
483
+ target[offset++] = (size >> 24) & 0xff
484
+ target.set(data, offset)
485
+ return offset + len
486
+ }
487
+ return offset
488
+ }
489
+
490
+ private static parseChunks (bytes: ArrayLike<number>): ScriptChunk[] {
491
+ const chunks: ScriptChunk[] = []
492
+ const length = bytes.length
493
+ let pos = 0
494
+ let inConditionalBlock = 0
495
+
496
+ while (pos < length) {
497
+ const op = bytes[pos++] ?? 0
498
+
499
+ if (op === OP.OP_RETURN && inConditionalBlock === 0) {
500
+ chunks.push({
501
+ op,
502
+ data: Script.copyRange(bytes, pos, length)
503
+ })
504
+ break
505
+ }
506
+
507
+ if (
508
+ op === OP.OP_IF ||
509
+ op === OP.OP_NOTIF ||
510
+ op === OP.OP_VERIF ||
511
+ op === OP.OP_VERNOTIF
512
+ ) {
513
+ inConditionalBlock++
514
+ } else if (op === OP.OP_ENDIF) {
515
+ inConditionalBlock--
516
+ }
517
+
518
+ if (op > 0 && op < OP.OP_PUSHDATA1) {
519
+ const len = op
520
+ const end = Math.min(pos + len, length)
521
+ chunks.push({
522
+ data: Script.copyRange(bytes, pos, end),
523
+ op
524
+ })
525
+ pos = end
526
+ } else if (op === OP.OP_PUSHDATA1) {
527
+ const len = pos < length ? bytes[pos++] ?? 0 : 0
528
+ const end = Math.min(pos + len, length)
529
+ chunks.push({
530
+ data: Script.copyRange(bytes, pos, end),
531
+ op
532
+ })
533
+ pos = end
534
+ } else if (op === OP.OP_PUSHDATA2) {
535
+ const b0 = bytes[pos] ?? 0
536
+ const b1 = bytes[pos + 1] ?? 0
537
+ const len = b0 | (b1 << 8)
538
+ pos = Math.min(pos + 2, length)
539
+ const end = Math.min(pos + len, length)
540
+ chunks.push({
541
+ data: Script.copyRange(bytes, pos, end),
542
+ op
543
+ })
544
+ pos = end
545
+ } else if (op === OP.OP_PUSHDATA4) {
546
+ const len =
547
+ ((bytes[pos] ?? 0) |
548
+ ((bytes[pos + 1] ?? 0) << 8) |
549
+ ((bytes[pos + 2] ?? 0) << 16) |
550
+ ((bytes[pos + 3] ?? 0) << 24)) >>>
551
+ 0
552
+ pos = Math.min(pos + 4, length)
553
+ const end = Math.min(pos + len, length)
554
+ chunks.push({
555
+ data: Script.copyRange(bytes, pos, end),
556
+ op
557
+ })
558
+ pos = end
559
+ } else {
560
+ chunks.push({ op })
561
+ }
562
+ }
563
+
564
+ return chunks
565
+ }
566
+
567
+ private static copyRange (
568
+ bytes: ArrayLike<number>,
569
+ start: number,
570
+ end: number
571
+ ): number[] {
572
+ const size = Math.max(end - start, 0)
573
+ const data = new Array(size)
574
+ for (let i = 0; i < size; i++) {
575
+ data[i] = bytes[start + i] ?? 0
576
+ }
577
+ return data
578
+ }
579
+
446
580
  private _chunkToString (chunk: ScriptChunk): string {
447
581
  const op = chunk.op
448
582
  let str = ''
@@ -7,7 +7,7 @@ import ScriptChunk from './ScriptChunk.js'
7
7
  import { minimallyEncode } from '../primitives/utils.js'
8
8
  import ScriptEvaluationError from './ScriptEvaluationError.js'
9
9
  import * as Hash from '../primitives/Hash.js'
10
- import TransactionSignature from '../primitives/TransactionSignature.js'
10
+ import TransactionSignature, { SignatureHashCache } from '../primitives/TransactionSignature.js'
11
11
  import PublicKey from '../primitives/PublicKey.js'
12
12
  import { verify } from '../primitives/ECDSA.js'
13
13
  import TransactionInput from '../transaction/TransactionInput.js'
@@ -152,6 +152,7 @@ export default class Spend {
152
152
  memoryLimit: number
153
153
  stackMem: number
154
154
  altStackMem: number
155
+ private sigHashCache: SignatureHashCache
155
156
 
156
157
  /**
157
158
  * @constructor
@@ -216,6 +217,7 @@ export default class Spend {
216
217
  this.ifStack = []
217
218
  this.stackMem = 0
218
219
  this.altStackMem = 0
220
+ this.sigHashCache = { hashOutputsSingle: new Map() }
219
221
  this.reset()
220
222
  }
221
223
 
@@ -228,6 +230,7 @@ export default class Spend {
228
230
  this.ifStack = []
229
231
  this.stackMem = 0
230
232
  this.altStackMem = 0
233
+ this.sigHashCache = { hashOutputsSingle: new Map() }
231
234
  }
232
235
 
233
236
  private ensureStackMem (additional: number): void {
@@ -353,7 +356,7 @@ export default class Spend {
353
356
  pubkey: PublicKey,
354
357
  subscript: Script
355
358
  ): boolean {
356
- const preimage = TransactionSignature.format({
359
+ const preimage = TransactionSignature.formatBytes({
357
360
  sourceTXID: this.sourceTXID,
358
361
  sourceOutputIndex: this.sourceOutputIndex,
359
362
  sourceSatoshis: this.sourceSatoshis,
@@ -364,7 +367,8 @@ export default class Spend {
364
367
  subscript,
365
368
  inputSequence: this.inputSequence,
366
369
  lockTime: this.lockTime,
367
- scope: sig.scope
370
+ scope: sig.scope,
371
+ cache: this.sigHashCache
368
372
  })
369
373
  const hash = new BigNumber(Hash.hash256(preimage))
370
374
  return verify(hash, sig, pubkey)
@@ -5,6 +5,9 @@ import BeefTx from './BeefTx.js'
5
5
  import { Reader, Writer, toHex, toArray, verifyNotNull } from '../primitives/utils.js'
6
6
  import { hash256 } from '../primitives/Hash.js'
7
7
 
8
+ const BufferCtor =
9
+ typeof globalThis !== 'undefined' ? (globalThis as any).Buffer : undefined
10
+
8
11
  export const BEEF_V1 = 4022206465 // 0100BEEF in LE order
9
12
  export const BEEF_V2 = 4022206466 // 0200BEEF in LE order
10
13
  export const ATOMIC_BEEF = 0x01010101 // 01010101
@@ -73,11 +76,49 @@ export class Beef {
73
76
  version: number = BEEF_V2
74
77
  atomicTxid: string | undefined = undefined
75
78
  private txidIndex: Map<string, BeefTx> | undefined = undefined
79
+ private rawBytesCache?: Uint8Array
80
+ private hexCache?: string
81
+ private needsSort: boolean = true
76
82
 
77
83
  constructor (version: number = BEEF_V2) {
78
84
  this.version = version
79
85
  }
80
86
 
87
+ private invalidateSerializationCaches (): void {
88
+ this.rawBytesCache = undefined
89
+ this.hexCache = undefined
90
+ }
91
+
92
+ private markMutated (requiresSort: boolean = true): void {
93
+ this.invalidateSerializationCaches()
94
+ if (requiresSort) {
95
+ this.needsSort = true
96
+ }
97
+ }
98
+
99
+ private ensureSerializableState (): void {
100
+ for (const tx of this.txs) {
101
+ void tx.txid
102
+ }
103
+ }
104
+
105
+ private ensureSortedForSerialization (): void {
106
+ if (this.needsSort) {
107
+ this.sortTxs()
108
+ }
109
+ }
110
+
111
+ private getSerializedBytes (): Uint8Array {
112
+ this.ensureSerializableState()
113
+ if (this.rawBytesCache == null) {
114
+ this.ensureSortedForSerialization()
115
+ const writer = new Writer()
116
+ this.toWriter(writer)
117
+ this.rawBytesCache = writer.toUint8Array()
118
+ }
119
+ return this.rawBytesCache
120
+ }
121
+
81
122
  /**
82
123
  * @param txid of `beefTx` to find
83
124
  * @returns `BeefTx` in `txs` with `txid`.
@@ -123,6 +164,7 @@ export class Beef {
123
164
  }
124
165
  this.deleteFromIndex(txid)
125
166
  this.txs.splice(i, 1)
167
+ this.markMutated(true)
126
168
  btx = this.mergeTxidOnly(txid)
127
169
  return btx
128
170
  }
@@ -208,6 +250,7 @@ export class Beef {
208
250
  * @returns index of merged bump
209
251
  */
210
252
  mergeBump (bump: MerklePath): number {
253
+ this.markMutated(false)
211
254
  let bumpIndex: number | undefined
212
255
  // If this proof is identical to another one previously added, we use that first. Otherwise, we try to merge it with proofs from the same block.
213
256
  for (let i = 0; i < this.bumps.length; i++) {
@@ -266,6 +309,7 @@ export class Beef {
266
309
  * @returns txid of rawTx
267
310
  */
268
311
  mergeRawTx (rawTx: number[], bumpIndex?: number): BeefTx {
312
+ this.markMutated(true)
269
313
  const newTx: BeefTx = new BeefTx(rawTx, bumpIndex)
270
314
  this.removeExistingTxid(newTx.txid)
271
315
  this.txs.push(newTx)
@@ -285,6 +329,7 @@ export class Beef {
285
329
  * @returns txid of tx
286
330
  */
287
331
  mergeTransaction (tx: Transaction): BeefTx {
332
+ this.markMutated(true)
288
333
  const txid = tx.id('hex')
289
334
  this.removeExistingTxid(txid)
290
335
  let bumpIndex: number | undefined
@@ -315,6 +360,7 @@ export class Beef {
315
360
  if (existingTxIndex >= 0) {
316
361
  this.deleteFromIndex(txid)
317
362
  this.txs.splice(existingTxIndex, 1)
363
+ this.markMutated(true)
318
364
  }
319
365
  }
320
366
 
@@ -325,6 +371,7 @@ export class Beef {
325
371
  this.txs.push(tx)
326
372
  this.addToIndex(tx)
327
373
  this.tryToValidateBumpIndex(tx)
374
+ this.markMutated(true)
328
375
  }
329
376
  return tx
330
377
  }
@@ -521,11 +568,11 @@ export class Beef {
521
568
  * @returns A binary array representing the BEEF
522
569
  */
523
570
  toBinary (): number[] {
524
- // Always serialize in dependency sorted order.
525
- this.sortTxs()
526
- const writer = new Writer()
527
- this.toWriter(writer)
528
- return writer.toArray()
571
+ return Array.from(this.getSerializedBytes())
572
+ }
573
+
574
+ toUint8Array (): Uint8Array {
575
+ return this.getSerializedBytes()
529
576
  }
530
577
 
531
578
  /**
@@ -539,7 +586,9 @@ export class Beef {
539
586
  * @returns serialized contents of this Beef with AtomicBEEF prefix.
540
587
  */
541
588
  toBinaryAtomic (txid: string): number[] {
542
- this.sortTxs()
589
+ if (this.needsSort) {
590
+ this.sortTxs()
591
+ }
543
592
  const tx = this.findTxid(txid)
544
593
  if (tx == null) {
545
594
  throw new Error(`${txid} does not exist in this Beef`)
@@ -566,7 +615,16 @@ export class Beef {
566
615
  * @returns A hex string representing the BEEF
567
616
  */
568
617
  toHex (): string {
569
- return toHex(this.toBinary())
618
+ if (this.hexCache != null) {
619
+ return this.hexCache
620
+ }
621
+ const bytes = this.getSerializedBytes()
622
+ const hex =
623
+ BufferCtor != null
624
+ ? BufferCtor.from(bytes).toString('hex')
625
+ : toHex(Array.from(bytes))
626
+ this.hexCache = hex
627
+ return hex
570
628
  }
571
629
 
572
630
  static fromReader (br: Reader): Beef {
@@ -745,6 +803,9 @@ export class Beef {
745
803
  .concat(txidOnly)
746
804
  .concat(result)
747
805
 
806
+ this.needsSort = false
807
+ this.invalidateSerializationCaches()
808
+
748
809
  return {
749
810
  missingInputs: Object.keys(missingInputs),
750
811
  notValid: txsNotValid.map((tx) => tx.txid),
@@ -763,6 +824,7 @@ export class Beef {
763
824
  c.bumps = Array.from(this.bumps)
764
825
  c.txs = Array.from(this.txs)
765
826
  c.txidIndex = undefined
827
+ c.needsSort = this.needsSort
766
828
  return c
767
829
  }
768
830
 
@@ -771,16 +833,21 @@ export class Beef {
771
833
  * @param knownTxids
772
834
  */
773
835
  trimKnownTxids (knownTxids: string[]): void {
836
+ let mutated = false
774
837
  for (let i = 0; i < this.txs.length;) {
775
838
  const tx = this.txs[i]
776
839
  if (tx.isTxidOnly && knownTxids.includes(tx.txid)) {
777
840
  this.deleteFromIndex(tx.txid)
778
841
  this.txs.splice(i, 1)
842
+ mutated = true
779
843
  } else {
780
844
  i++
781
845
  }
782
846
  }
783
847
  // TODO: bumps could be trimmed to eliminate unreferenced proofs.
848
+ if (mutated) {
849
+ this.markMutated(true)
850
+ }
784
851
  }
785
852
 
786
853
  /**
@@ -133,7 +133,7 @@ export default class BeefTx {
133
133
  if (this._rawTx != null) {
134
134
  writer.write(this._rawTx)
135
135
  } else if (this._tx != null) {
136
- writer.write(this._tx.toBinary())
136
+ writer.write(this._tx.toUint8Array())
137
137
  } else {
138
138
  throw new Error('a valid serialized Transaction is expected')
139
139
  }