@btc-vision/transaction 1.6.1 → 1.6.5

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 (75) hide show
  1. package/browser/_version.d.ts +1 -1
  2. package/browser/epoch/ChallengeSolution.d.ts +3 -3
  3. package/browser/epoch/validator/EpochValidator.d.ts +5 -6
  4. package/browser/generators/builders/P2WDAGenerator.d.ts +13 -0
  5. package/browser/index.js +1 -1
  6. package/browser/keypair/Address.d.ts +3 -2
  7. package/browser/keypair/AddressVerificator.d.ts +13 -1
  8. package/browser/keypair/Wallet.d.ts +3 -0
  9. package/browser/opnet.d.ts +4 -0
  10. package/browser/p2wda/P2WDADetector.d.ts +16 -0
  11. package/browser/transaction/TransactionFactory.d.ts +3 -1
  12. package/browser/transaction/builders/DeploymentTransaction.d.ts +3 -3
  13. package/browser/transaction/builders/InteractionTransactionP2WDA.d.ts +37 -0
  14. package/browser/transaction/builders/SharedInteractionTransaction.d.ts +4 -4
  15. package/browser/transaction/builders/TransactionBuilder.d.ts +3 -0
  16. package/browser/transaction/interfaces/ITransactionParameters.d.ts +1 -0
  17. package/browser/transaction/mineable/IP2WSHAddress.d.ts +4 -0
  18. package/browser/transaction/mineable/TimelockGenerator.d.ts +2 -5
  19. package/browser/transaction/shared/TweakedTransaction.d.ts +23 -0
  20. package/build/_version.d.ts +1 -1
  21. package/build/_version.js +1 -1
  22. package/build/epoch/ChallengeSolution.d.ts +3 -3
  23. package/build/epoch/ChallengeSolution.js +3 -3
  24. package/build/epoch/validator/EpochValidator.d.ts +5 -6
  25. package/build/epoch/validator/EpochValidator.js +11 -12
  26. package/build/generators/builders/P2WDAGenerator.d.ts +13 -0
  27. package/build/generators/builders/P2WDAGenerator.js +62 -0
  28. package/build/keypair/Address.d.ts +3 -2
  29. package/build/keypair/Address.js +28 -2
  30. package/build/keypair/AddressVerificator.d.ts +13 -1
  31. package/build/keypair/AddressVerificator.js +82 -1
  32. package/build/keypair/Wallet.d.ts +3 -0
  33. package/build/keypair/Wallet.js +4 -0
  34. package/build/opnet.d.ts +4 -0
  35. package/build/opnet.js +4 -0
  36. package/build/p2wda/P2WDADetector.d.ts +16 -0
  37. package/build/p2wda/P2WDADetector.js +97 -0
  38. package/build/transaction/TransactionFactory.d.ts +3 -1
  39. package/build/transaction/TransactionFactory.js +35 -4
  40. package/build/transaction/builders/DeploymentTransaction.d.ts +3 -3
  41. package/build/transaction/builders/DeploymentTransaction.js +1 -1
  42. package/build/transaction/builders/InteractionTransactionP2WDA.d.ts +37 -0
  43. package/build/transaction/builders/InteractionTransactionP2WDA.js +205 -0
  44. package/build/transaction/builders/SharedInteractionTransaction.d.ts +4 -4
  45. package/build/transaction/builders/SharedInteractionTransaction.js +3 -3
  46. package/build/transaction/builders/TransactionBuilder.d.ts +3 -0
  47. package/build/transaction/builders/TransactionBuilder.js +18 -3
  48. package/build/transaction/interfaces/ITransactionParameters.d.ts +1 -0
  49. package/build/transaction/mineable/IP2WSHAddress.d.ts +4 -0
  50. package/build/transaction/mineable/IP2WSHAddress.js +1 -0
  51. package/build/transaction/mineable/TimelockGenerator.d.ts +2 -5
  52. package/build/transaction/shared/TweakedTransaction.d.ts +23 -0
  53. package/build/transaction/shared/TweakedTransaction.js +154 -18
  54. package/doc/README.md +0 -0
  55. package/doc/addresses/P2OP.md +1 -0
  56. package/doc/addresses/P2WDA.md +240 -0
  57. package/package.json +2 -2
  58. package/src/_version.ts +1 -1
  59. package/src/epoch/ChallengeSolution.ts +4 -4
  60. package/src/epoch/validator/EpochValidator.ts +12 -16
  61. package/src/generators/builders/P2WDAGenerator.ts +174 -0
  62. package/src/keypair/Address.ts +58 -3
  63. package/src/keypair/AddressVerificator.ts +147 -2
  64. package/src/keypair/Wallet.ts +16 -0
  65. package/src/opnet.ts +4 -0
  66. package/src/p2wda/P2WDADetector.ts +218 -0
  67. package/src/transaction/TransactionFactory.ts +79 -5
  68. package/src/transaction/builders/DeploymentTransaction.ts +4 -3
  69. package/src/transaction/builders/InteractionTransactionP2WDA.ts +376 -0
  70. package/src/transaction/builders/SharedInteractionTransaction.ts +7 -6
  71. package/src/transaction/builders/TransactionBuilder.ts +30 -3
  72. package/src/transaction/interfaces/ITransactionParameters.ts +1 -0
  73. package/src/transaction/mineable/IP2WSHAddress.ts +4 -0
  74. package/src/transaction/mineable/TimelockGenerator.ts +2 -6
  75. package/src/transaction/shared/TweakedTransaction.ts +246 -23
@@ -1,12 +1,21 @@
1
1
  import { Logger } from '@btc-vision/logger';
2
- import { address as bitAddress, crypto as bitCrypto, getFinalScripts, isP2MS, isP2PK, isP2PKH, isP2SHScript, isP2TR, isP2WPKH, isP2WSHScript, isUnknownSegwitVersion, opcodes, payments, PaymentType, script, toXOnly, varuint, } from '@btc-vision/bitcoin';
2
+ import { address as bitAddress, crypto as bitCrypto, getFinalScripts, isP2A, isP2MS, isP2PK, isP2PKH, isP2SHScript, isP2TR, isP2WPKH, isP2WSHScript, isUnknownSegwitVersion, opcodes, payments, PaymentType, script, toXOnly, varuint, } from '@btc-vision/bitcoin';
3
3
  import { TweakedSigner } from '../../signer/TweakedSigner.js';
4
4
  import { canSignNonTaprootInput, isTaprootInput, pubkeyInScript, } from '../../signer/SignerUtils.js';
5
+ import { TransactionBuilder } from '../builders/TransactionBuilder.js';
6
+ import { Buffer } from 'buffer';
7
+ import { P2WDADetector } from '../../p2wda/P2WDADetector.js';
5
8
  export var TransactionSequence;
6
9
  (function (TransactionSequence) {
7
10
  TransactionSequence[TransactionSequence["REPLACE_BY_FEE"] = 4294967293] = "REPLACE_BY_FEE";
8
11
  TransactionSequence[TransactionSequence["FINAL"] = 4294967295] = "FINAL";
9
12
  })(TransactionSequence || (TransactionSequence = {}));
13
+ export var CSVModes;
14
+ (function (CSVModes) {
15
+ CSVModes[CSVModes["BLOCKS"] = 0] = "BLOCKS";
16
+ CSVModes[CSVModes["TIMESTAMPS"] = 1] = "TIMESTAMPS";
17
+ })(CSVModes || (CSVModes = {}));
18
+ const CSV_ENABLED_BLOCKS_MASK = 0x3fffffff;
10
19
  export class TweakedTransaction extends Logger {
11
20
  constructor(data) {
12
21
  super();
@@ -19,9 +28,12 @@ export class TweakedTransaction extends Logger {
19
28
  this.sequence = TransactionSequence.REPLACE_BY_FEE;
20
29
  this.tapLeafScript = null;
21
30
  this.isBrowser = false;
31
+ this.csvInputIndices = new Set();
32
+ this.anchorInputIndices = new Set();
22
33
  this.regenerated = false;
23
34
  this.ignoreSignatureErrors = false;
24
35
  this.noSignatures = false;
36
+ this.txVersion = 2;
25
37
  this.customFinalizerP2SH = (inputIndex, input, scriptA, isSegwit, isP2SH, isP2WSH) => {
26
38
  const inputDecoded = this.inputs[inputIndex];
27
39
  if (isP2SH && input.partialSig && inputDecoded && inputDecoded.redeemScript) {
@@ -32,6 +44,29 @@ export class TweakedTransaction extends Logger {
32
44
  finalScriptWitness: undefined,
33
45
  };
34
46
  }
47
+ if (this.anchorInputIndices.has(inputIndex)) {
48
+ return {
49
+ finalScriptSig: undefined,
50
+ finalScriptWitness: Buffer.from([0]),
51
+ };
52
+ }
53
+ if (isP2WSH && isSegwit && input.witnessScript) {
54
+ if (!input.partialSig || input.partialSig.length === 0) {
55
+ throw new Error(`No signatures for P2WSH input #${inputIndex}`);
56
+ }
57
+ const isP2WDA = P2WDADetector.isP2WDAWitnessScript(input.witnessScript);
58
+ if (isP2WDA) {
59
+ return this.finalizeSecondaryP2WDA(inputIndex, input);
60
+ }
61
+ const isCSVInput = this.csvInputIndices.has(inputIndex);
62
+ if (isCSVInput) {
63
+ const witnessStack = [input.partialSig[0].signature, input.witnessScript];
64
+ return {
65
+ finalScriptSig: undefined,
66
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witnessStack),
67
+ };
68
+ }
69
+ }
35
70
  return getFinalScripts(inputIndex, input, scriptA, isSegwit, isP2SH, isP2WSH, true, this.unlockScript);
36
71
  };
37
72
  this.signer = data.signer;
@@ -40,6 +75,9 @@ export class TweakedTransaction extends Logger {
40
75
  this.nonWitnessUtxo = data.nonWitnessUtxo;
41
76
  this.unlockScript = data.unlockScript;
42
77
  this.isBrowser = typeof window !== 'undefined';
78
+ if (data.txVersion) {
79
+ this.txVersion = data.txVersion;
80
+ }
43
81
  }
44
82
  static readScriptWitnessToWitnessStack(Buffer) {
45
83
  let offset = 0;
@@ -120,6 +158,9 @@ export class TweakedTransaction extends Logger {
120
158
  throw new Error('Transaction is already signed');
121
159
  this.sequence = TransactionSequence.FINAL;
122
160
  for (const input of this.inputs) {
161
+ if (this.csvInputIndices.has(this.inputs.indexOf(input))) {
162
+ continue;
163
+ }
123
164
  input.sequence = TransactionSequence.FINAL;
124
165
  }
125
166
  }
@@ -157,6 +198,8 @@ export class TweakedTransaction extends Logger {
157
198
  return this.signer;
158
199
  }
159
200
  async signInput(transaction, input, i, signer, reverse = false, errored = false) {
201
+ if (this.anchorInputIndices.has(i))
202
+ return;
160
203
  const publicKey = signer.publicKey;
161
204
  let isTaproot = isTaprootInput(input);
162
205
  if (reverse) {
@@ -335,17 +378,17 @@ export class TweakedTransaction extends Logger {
335
378
  return;
336
379
  }
337
380
  generatePsbtInputExtended(utxo, i, _extra = false) {
338
- const script = Buffer.from(utxo.scriptPubKey.hex, 'hex');
381
+ const scriptPub = Buffer.from(utxo.scriptPubKey.hex, 'hex');
339
382
  const input = {
340
383
  hash: utxo.transactionId,
341
384
  index: utxo.outputIndex,
342
385
  sequence: this.sequence,
343
386
  witnessUtxo: {
344
387
  value: Number(utxo.value),
345
- script,
388
+ script: scriptPub,
346
389
  },
347
390
  };
348
- if (isP2PKH(script)) {
391
+ if (isP2PKH(scriptPub)) {
349
392
  if (utxo.nonWitnessUtxo) {
350
393
  input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
351
394
  ? utxo.nonWitnessUtxo
@@ -355,17 +398,12 @@ export class TweakedTransaction extends Logger {
355
398
  throw new Error('Missing nonWitnessUtxo for P2PKH UTXO');
356
399
  }
357
400
  }
358
- else if (isP2WPKH(script) || isUnknownSegwitVersion(script)) {
401
+ else if (isP2WPKH(scriptPub) || isUnknownSegwitVersion(scriptPub)) {
359
402
  }
360
- else if (isP2WSHScript(script)) {
361
- if (!utxo.witnessScript) {
362
- throw new Error('Missing witnessScript for P2WSH UTXO');
363
- }
364
- input.witnessScript = Buffer.isBuffer(utxo.witnessScript)
365
- ? utxo.witnessScript
366
- : Buffer.from(utxo.witnessScript, 'hex');
403
+ else if (isP2WSHScript(scriptPub)) {
404
+ this.processP2WSHInput(utxo, input, i);
367
405
  }
368
- else if (isP2SHScript(script)) {
406
+ else if (isP2SHScript(scriptPub)) {
369
407
  let redeemScriptBuf;
370
408
  if (utxo.redeemScript) {
371
409
  redeemScriptBuf = Buffer.isBuffer(utxo.redeemScript)
@@ -401,15 +439,13 @@ export class TweakedTransaction extends Logger {
401
439
  }
402
440
  else if (isP2WSHScript(redeemOutput)) {
403
441
  delete input.nonWitnessUtxo;
404
- if (!input.witnessScript) {
405
- throw new Error('Missing witnessScript for P2SH-P2WSH UTXO');
406
- }
442
+ this.processP2WSHInput(utxo, input, i);
407
443
  }
408
444
  else {
409
445
  delete input.witnessUtxo;
410
446
  }
411
447
  }
412
- else if (isP2TR(script)) {
448
+ else if (isP2TR(scriptPub)) {
413
449
  if (this.sighashTypes) {
414
450
  const inputSign = TweakedTransaction.calculateSignHash(this.sighashTypes);
415
451
  if (inputSign)
@@ -418,7 +454,11 @@ export class TweakedTransaction extends Logger {
418
454
  this.tweakSigner();
419
455
  input.tapInternalKey = this.internalPubKeyToXOnly();
420
456
  }
421
- else if (isP2PK(script) || isP2MS(script)) {
457
+ else if (isP2A(scriptPub)) {
458
+ this.anchorInputIndices.add(i);
459
+ input.isPayToAnchor = true;
460
+ }
461
+ else if (isP2PK(scriptPub) || isP2MS(scriptPub)) {
422
462
  if (utxo.nonWitnessUtxo) {
423
463
  input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
424
464
  ? utxo.nonWitnessUtxo
@@ -441,6 +481,53 @@ export class TweakedTransaction extends Logger {
441
481
  }
442
482
  return input;
443
483
  }
484
+ processP2WSHInput(utxo, input, i) {
485
+ if (!utxo.witnessScript) {
486
+ throw new Error('Missing witnessScript for P2WSH UTXO');
487
+ }
488
+ input.witnessScript = Buffer.isBuffer(utxo.witnessScript)
489
+ ? utxo.witnessScript
490
+ : Buffer.from(utxo.witnessScript, 'hex');
491
+ const decompiled = script.decompile(input.witnessScript);
492
+ if (decompiled && this.isCSVScript(decompiled)) {
493
+ const decompiled = script.decompile(input.witnessScript);
494
+ if (decompiled && this.isCSVScript(decompiled)) {
495
+ this.csvInputIndices.add(i);
496
+ const csvBlocks = this.extractCSVBlocks(decompiled);
497
+ console.log('csvBlocks', csvBlocks);
498
+ input.sequence = this.setCSVSequence(csvBlocks, this.sequence);
499
+ }
500
+ }
501
+ }
502
+ secondsToCSVTimeUnits(seconds) {
503
+ return Math.floor(seconds / 512);
504
+ }
505
+ createTimeBasedCSV(seconds) {
506
+ const timeUnits = this.secondsToCSVTimeUnits(seconds);
507
+ if (timeUnits > 0xffff) {
508
+ throw new Error(`Time units ${timeUnits} exceeds maximum of 65,535`);
509
+ }
510
+ return timeUnits | (1 << 22);
511
+ }
512
+ isCSVEnabled(sequence) {
513
+ return (sequence & (1 << 31)) === 0;
514
+ }
515
+ extractCSVValue(sequence) {
516
+ return sequence & 0x0000ffff;
517
+ }
518
+ finalizeSecondaryP2WDA(inputIndex, input) {
519
+ if (!input.partialSig || input.partialSig.length === 0) {
520
+ throw new Error(`No signature for P2WDA input #${inputIndex}`);
521
+ }
522
+ if (!input.witnessScript) {
523
+ throw new Error(`No witness script for P2WDA input #${inputIndex}`);
524
+ }
525
+ const witnessStack = P2WDADetector.createSimpleP2WDAWitness(input.partialSig[0].signature, input.witnessScript);
526
+ return {
527
+ finalScriptSig: undefined,
528
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witnessStack),
529
+ };
530
+ }
444
531
  async signInputsWalletBased(transaction) {
445
532
  const signer = this.signer;
446
533
  await signer.multiSignPsbt([transaction]);
@@ -449,6 +536,55 @@ export class TweakedTransaction extends Logger {
449
536
  }
450
537
  this.finalized = true;
451
538
  }
539
+ isCSVScript(decompiled) {
540
+ return decompiled.some((op) => op === opcodes.OP_CHECKSEQUENCEVERIFY);
541
+ }
542
+ setCSVSequence(csvBlocks, currentSequence) {
543
+ if (this.txVersion < 2) {
544
+ throw new Error('CSV requires transaction version 2 or higher');
545
+ }
546
+ if (csvBlocks > 0xffff) {
547
+ throw new Error(`CSV blocks ${csvBlocks} exceeds maximum of 65,535`);
548
+ }
549
+ const isTimeBased = (csvBlocks & (1 << 22)) !== 0;
550
+ let sequence = csvBlocks & 0x0000ffff;
551
+ if (isTimeBased) {
552
+ sequence |= 1 << 22;
553
+ }
554
+ if (currentSequence === TransactionSequence.REPLACE_BY_FEE) {
555
+ sequence |= 1 << 25;
556
+ }
557
+ sequence = sequence & 0x7fffffff;
558
+ return sequence;
559
+ }
560
+ getCSVType(csvValue) {
561
+ return csvValue & (1 << 22) ? CSVModes.TIMESTAMPS : CSVModes.BLOCKS;
562
+ }
563
+ extractCSVBlocks(decompiled) {
564
+ for (let i = 0; i < decompiled.length; i++) {
565
+ if (decompiled[i] === opcodes.OP_CHECKSEQUENCEVERIFY && i > 0) {
566
+ const csvValue = decompiled[i - 1];
567
+ if (Buffer.isBuffer(csvValue)) {
568
+ return script.number.decode(csvValue);
569
+ }
570
+ else if (typeof csvValue === 'number') {
571
+ if (csvValue === opcodes.OP_0 || csvValue === opcodes.OP_FALSE) {
572
+ return 0;
573
+ }
574
+ else if (csvValue === opcodes.OP_1NEGATE) {
575
+ return -1;
576
+ }
577
+ else if (csvValue >= opcodes.OP_1 && csvValue <= opcodes.OP_16) {
578
+ return csvValue - opcodes.OP_1 + 1;
579
+ }
580
+ else {
581
+ throw new Error(`Unexpected raw number in script: ${csvValue}`);
582
+ }
583
+ }
584
+ }
585
+ }
586
+ return 0;
587
+ }
452
588
  async attemptSignTaproot(transaction, input, i, signer, publicKey) {
453
589
  const isScriptSpend = this.isTaprootScriptSpend(input, publicKey);
454
590
  if (isScriptSpend) {
package/doc/README.md ADDED
File without changes
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,240 @@
1
+ # Pay-to-Witness-Data-Authentication (P2WDA)
2
+
3
+ ## Executive Summary
4
+
5
+ Bitcoin makes arbitrary authenticated data inclusion expensive unless you exploit the SegWit witness discount, where
6
+ every witness byte weighs 1 unit instead of 4. P2WDA is a spend template that deliberately carries authenticated
7
+ application data in witness stack items and then drops them before a standard signature check. This preserves standard
8
+ Bitcoin validation while letting applications verify the attached data out-of-band.
9
+
10
+ In this implementation, the witness stack reserves **10 data slots** per P2WDA input, each **≤80 bytes** to respect
11
+ default relay policy. The data (after compression) is prefixed by a **BIP340 Schnorr** signature over
12
+ `(tx_signature || uncompressed_data)`, then split across those slots. Applications reassemble and verify the signed
13
+ data, making any miner tampering detectable. On-chain validation remains standard and cheap thanks to the witness
14
+ discount.
15
+
16
+ ## 1. Problem Space
17
+
18
+ ### 1.1 The Traditional Dilemma
19
+
20
+ OP_RETURN is simple but tiny: **80 bytes max** per output under standard policy. Anything larger forces many outputs and
21
+ multiple transactions, compounding base (non-witness) bytes that weigh 4× witness bytes. For realistic payloads (
22
+ airdrops, rich metadata), this is cost-prohibitive.
23
+
24
+ Importantly, even if OP_RETURN size limits were completely removed, it would not solve the fundamental economic problem.
25
+ OP_RETURN data is stored in the transaction's output section, which means every byte counts as non-witness data and
26
+ incurs the full 4× weight penalty. A hypothetical uncapped OP_RETURN storing 800 bytes would cost 3,200 weight units,
27
+ while P2WDA achieves the same data storage for only 800 weight units in the witness section. The economic disadvantage
28
+ of OP_RETURN is architectural, not merely a policy limitation.
29
+
30
+ Commit-reveal styles fix integrity but double touches the chain (commit tx, reveal tx) and fragment UX.
31
+
32
+ ### 1.2 The Witness Discount Opportunity
33
+
34
+ SegWit introduced **weight**: non-witness bytes weigh 4 units each; witness bytes weigh **1**. Fees are proportional to
35
+ weight (vbytes ≈ weight/4). Packing authenticated data into witness can be ~75% cheaper than encoding the same data in
36
+ base tx bytes.
37
+
38
+ ## 2. Technical Architecture
39
+
40
+ ### 2.1 Spend Template
41
+
42
+ P2WDA uses a **P2WSH** spend whose script pre-drops a fixed number of stack items (the data slots), then performs a
43
+ standard single-sig check.
44
+
45
+ The witness script consists of:
46
+
47
+ - 5 consecutive `OP_2DROP` operations (dropping 10 items total)
48
+ - A 33-byte compressed public key
49
+ - An `OP_CHECKSIG` operation
50
+
51
+ This creates a script of approximately 40 bytes that validates like any standard single-signature P2WSH spend, but with
52
+ space for our data payload in the witness stack.
53
+
54
+ ### 2.2 Authentication & Packing
55
+
56
+ The authentication and packing process ensures data integrity while maximizing compression efficiency. Here's how it
57
+ works:
58
+
59
+ **Step 1: Prepare the data**
60
+
61
+ - Start with your uncompressed payload data (application bytes)
62
+ - Get the transaction signature (the DER signature that authorizes spending this input)
63
+
64
+ **Step 2: Create authentication signature**
65
+
66
+ - Compute a BIP340 Schnorr signature over the hash of (transaction_signature || payload_data)
67
+ - This signature proves authorship and binds the data to this specific spend
68
+
69
+ **Step 3: Combine and compress**
70
+
71
+ ```ts
72
+ // Combine the authentication signature with the payload
73
+ const combined_bytes = data_signature + payload_data;
74
+
75
+ // Compress everything using DEFLATE or similar
76
+ const compressed_bytes = COMPRESS(combined_bytes);
77
+
78
+ // Split into chunks of max 80 bytes each
79
+ const chunks = SPLIT_INTO_80_BYTE_CHUNKS(compressed_bytes);
80
+
81
+ // Ensure we don't exceed 10 chunks
82
+ if (chunks.length > 10) {
83
+ throw Error("Payload too large")
84
+ }
85
+ ```
86
+
87
+ **Step 4: Build the witness stack**
88
+ The witness stack must contain exactly 12 items in this order:
89
+
90
+ 1. Data slot 0 (up to 80 bytes, or empty)
91
+ 2. Data slot 1 (up to 80 bytes, or empty)
92
+ 3. ... through Data slot 9
93
+ 4. Transaction signature (DER encoded, ~72 bytes)
94
+ 5. Witness script (~40 bytes)
95
+
96
+ Any unused data slots are filled with empty byte arrays (length 0) to maintain the expected stack structure.
97
+
98
+ **Verification Process for Indexers:**
99
+
100
+ When an indexer encounters a P2WDA spend, it:
101
+
102
+ 1. Extracts and concatenates the first 10 witness items
103
+ 2. Decompresses the result to get (data_signature || original_data)
104
+ 3. Verifies the Schnorr signature against hash(tx_signature || original_data)
105
+ 4. If valid, passes the original_data to the application layer
106
+
107
+ This design ensures that while Bitcoin consensus never inspects the data, any tampering is cryptographically detectable
108
+ by applications.
109
+
110
+ ### 2.3 Input Placement Rules
111
+
112
+ To simplify parsing and minimize duplication:
113
+
114
+ * **All application data must be injected in the first P2WDA input by index** (the lowest-index input spending a P2WDA
115
+ UTXO)
116
+ * Any **additional P2WDA inputs** in the same tx must supply **10 empty data items** (length=0) in their witness
117
+ * Non-P2WDA inputs are unaffected
118
+ * If a transaction flagged as `InteractionTransactionP2WDA` spends **no** P2WDA UTXOs, **throw an error**
119
+
120
+ ## 3. Economics
121
+
122
+ ### 3.1 OP_RETURN Baseline
123
+
124
+ Standard OP_RETURN script is ≤83 bytes total, allowing ≤80 bytes data. All bytes are non-witness, costing **1 vbyte per
125
+ byte**. For 80 bytes of data you typically consume ~91 vbytes including script overhead and output framing.
126
+
127
+ ### 3.2 P2WDA Spend
128
+
129
+ The witness bytes calculation for a P2WDA input includes:
130
+
131
+ - 1 byte for item count
132
+ - 10 bytes for length prefixes (one per data slot)
133
+ - The actual compressed data bytes
134
+ - ~73 bytes for the transaction signature (including length prefix)
135
+ - ~41 bytes for the witness script (including length prefix)
136
+
137
+ Since all of this is witness data, it costs only 1/4 the weight of equivalent non-witness bytes.
138
+
139
+ **Example with 512 bytes of incompressible data:**
140
+
141
+ - Data + signature = 576 bytes total
142
+ - Split across 8 slots (72 bytes each)
143
+ - Total witness bytes ≈ 701
144
+ - Cost in vbytes ≈ 175
145
+
146
+ Compare to OP_RETURN which would need 7 outputs at ~91 vbytes each ≈ 637 vbytes.
147
+ **Result: ~72% savings** while keeping everything in a single transaction.
148
+
149
+ ## 4. Security Model
150
+
151
+ The security model separates on-chain authorization from off-chain data authentication:
152
+
153
+ * **On-chain authorization**: The transaction signature proves spend authorization exactly like any P2WSH single-sig
154
+ spend
155
+ * **Off-chain authorship**: The Schnorr signature authenticates the data and binds it to this spend via the transaction
156
+ signature
157
+ * **Malleability handling**: SegWit allows witness data modification without changing the txid. The Schnorr signature
158
+ ensures any tampering is detectable
159
+
160
+ Practical outcomes:
161
+
162
+ - Funds cannot be redirected (protected by transaction signature)
163
+ - Data forgery is detectable at the application layer (Schnorr signature fails)
164
+ - Miners could theoretically replace data with garbage, but applications will reject it
165
+
166
+ ## 5. Operational Capabilities
167
+
168
+ P2WDA works well for:
169
+
170
+ * Mints
171
+ * Airdrops
172
+ * NFTs
173
+ * Batch updates and state checkpoints
174
+ * Governance votes and attestations
175
+ * Swap listings (like nativeswap) but **NOT TRADES**, a miner could cancel your transaction!
176
+ * etc.
177
+
178
+ The 10 slots serve as a transport layer; keep application formats versioned and compact.
179
+
180
+ ## 6. Advanced Considerations
181
+
182
+ ### 6.1 Why 10 Slots of ≤80 Bytes?
183
+
184
+ Default relay policy limits:
185
+
186
+ - Maximum 100 stack items for P2WSH
187
+ - Maximum 80 bytes per non-script witness item
188
+ - Maximum 3600 bytes for the witness script itself
189
+
190
+ Ten slots provides headroom while keeping scripts simple (5 * OP_2DROP). You can scale by using multiple P2WDA inputs,
191
+ keeping data only in the first P2WDA input and zeros in the rest.
192
+
193
+ ### 6.2 Domain Separation (Recommended)
194
+
195
+ To harden against cross-protocol attacks, consider using domain separation:
196
+
197
+ ```
198
+ message = Hash("P2WDA/v1" || txid || input_index || tx_signature || data)
199
+ data_signature = Schnorr.Sign(auth_private_key, message)
200
+ ```
201
+
202
+ This prevents signatures from being reused across different protocols or transactions.
203
+
204
+ ### 6.3 Compression
205
+
206
+ Use a standard compression algorithm like DEFLATE to maximize data packing efficiency. Ensure your application layer
207
+ can handle decompression and verify the integrity of the decompressed data.
208
+
209
+ ### 6.4 Why SegWit Instead of Taproot?
210
+
211
+ A common question is why P2WDA uses SegWit's P2WSH instead of the newer Taproot technology. This decision is deliberate
212
+ and based on fundamental economics of block space usage. Understanding this choice illuminates the elegant design of
213
+ P2WDA.
214
+
215
+ #### The Block Space Reality
216
+
217
+ When people first hear about P2WDA, they might assume it should use Taproot since it's Bitcoin's newest upgrade.
218
+ However, Taproot would actually consume more block space for our use case, making it less efficient. This
219
+ counterintuitive reality stems from how these technologies were designed for different purposes.
220
+
221
+ Taproot offers two spending paths. The key path is remarkably efficient, requiring only a 64-byte Schnorr signature, but
222
+ it cannot carry arbitrary data. To include data, you must use the script path, which requires a control block containing
223
+ the internal public key, parity information, and Merkle proof of your script's inclusion in the taproot tree. Even with
224
+ the simplest possible tree structure, this control block adds approximately 65 bytes of pure overhead.
225
+
226
+ Let's examine the concrete numbers for storing 500 bytes of authenticated data:
227
+
228
+ With P2WSH (what P2WDA uses), the witness contains the transaction signature (~72 bytes), your data (500 bytes), and the
229
+ witness script (~40 bytes), totaling approximately 612 bytes, which equals 612 weight units.
230
+
231
+ With Taproot's script path, you would need a Schnorr signature (64 bytes), your data (500 bytes), the tapscript (~40
232
+ bytes), and the control block (~65 bytes), totaling approximately 669 bytes, which equals 669 weight units.
233
+
234
+ This represents about 10% more block space consumption for identical functionality. When your goal is cost-efficient
235
+ data storage, every byte matters, and this overhead directly translates to higher fees for users.
236
+
237
+ ### 6.4 Future Extensions
238
+
239
+ - Use annexes (BIP490) to carry larger payloads if needed and stop requiring another signature in the witness stack
240
+ - Explore multi-signature variants for collaborative data signing
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@btc-vision/transaction",
3
3
  "type": "module",
4
- "version": "1.6.1",
4
+ "version": "1.6.5",
5
5
  "author": "BlobMaster41",
6
6
  "description": "OPNet transaction library allows you to create and sign transactions for the OPNet network.",
7
7
  "engines": {
@@ -89,7 +89,7 @@
89
89
  "dependencies": {
90
90
  "@babel/plugin-proposal-object-rest-spread": "^7.21.4-esm",
91
91
  "@bitcoinerlab/secp256k1": "^1.2.0",
92
- "@btc-vision/bitcoin": "^6.4.6",
92
+ "@btc-vision/bitcoin": "^6.4.8",
93
93
  "@btc-vision/bitcoin-rpc": "^1.0.2",
94
94
  "@btc-vision/logger": "^1.0.6",
95
95
  "@eslint/js": "^9.32.0",
package/src/_version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '1.6.1';
1
+ export const version = '1.6.5';
@@ -90,7 +90,7 @@ export class ChallengeSolution implements IChallengeSolution {
90
90
  /**
91
91
  * Static method to validate from raw data directly
92
92
  */
93
- public static async validateRaw(data: RawChallenge): Promise<boolean> {
93
+ public static validateRaw(data: RawChallenge): boolean {
94
94
  return EpochValidator.validateEpochWinner(data);
95
95
  }
96
96
 
@@ -116,9 +116,9 @@ export class ChallengeSolution implements IChallengeSolution {
116
116
 
117
117
  /**
118
118
  * Verify this challenge
119
- * @returns {Promise<boolean>} True if the challenge is valid
119
+ * @returns {boolean} True if the challenge is valid
120
120
  */
121
- public async verify(): Promise<boolean> {
121
+ public verify(): boolean {
122
122
  return EpochValidator.validateChallengeSolution(this);
123
123
  }
124
124
 
@@ -165,7 +165,7 @@ export class ChallengeSolution implements IChallengeSolution {
165
165
  * Calculate the expected solution hash for this challenge
166
166
  * @returns {Promise<Buffer>} The calculated solution hash
167
167
  */
168
- public async calculateSolution(): Promise<Buffer> {
168
+ public calculateSolution(): Buffer {
169
169
  return EpochValidator.calculateSolution(
170
170
  this.verification.targetChecksum,
171
171
  this.publicKey.toBuffer(),
@@ -1,9 +1,9 @@
1
1
  import { IChallengeSolution, RawChallenge } from '../interfaces/IChallengeSolution.js';
2
2
  import { ChallengeSolution } from '../ChallengeSolution.js';
3
+ import { crypto } from '@btc-vision/bitcoin';
3
4
 
4
5
  export class EpochValidator {
5
6
  private static readonly BLOCKS_PER_EPOCH: bigint = 5n;
6
- private static readonly GRAFFITI_LENGTH: number = 16;
7
7
 
8
8
  /**
9
9
  * Convert Buffer to Uint8Array
@@ -22,9 +22,8 @@ export class EpochValidator {
22
22
  /**
23
23
  * Calculate SHA-1 hash
24
24
  */
25
- public static async sha1(data: Uint8Array): Promise<Uint8Array> {
26
- const hashBuffer = await crypto.subtle.digest('SHA-1', data);
27
- return new Uint8Array(hashBuffer);
25
+ public static sha1(data: Uint8Array | Buffer): Buffer {
26
+ return crypto.sha1(Buffer.isBuffer(data) ? data : Buffer.from(data));
28
27
  }
29
28
 
30
29
  /**
@@ -78,10 +77,7 @@ export class EpochValidator {
78
77
  /**
79
78
  * Verify an epoch solution using IPreimage
80
79
  */
81
- public static async verifySolution(
82
- challenge: IChallengeSolution,
83
- log: boolean = false,
84
- ): Promise<boolean> {
80
+ public static verifySolution(challenge: IChallengeSolution, log: boolean = false): boolean {
85
81
  try {
86
82
  const verification = challenge.verification;
87
83
  const calculatedPreimage = this.calculatePreimage(
@@ -90,7 +86,7 @@ export class EpochValidator {
90
86
  challenge.salt,
91
87
  );
92
88
 
93
- const computedSolution = await this.sha1(this.bufferToUint8Array(calculatedPreimage));
89
+ const computedSolution = this.sha1(calculatedPreimage);
94
90
  const computedSolutionBuffer = this.uint8ArrayToBuffer(computedSolution);
95
91
 
96
92
  if (!computedSolutionBuffer.equals(challenge.solution)) {
@@ -134,16 +130,16 @@ export class EpochValidator {
134
130
  /**
135
131
  * Validate epoch winner from raw data
136
132
  */
137
- public static async validateEpochWinner(epochData: RawChallenge): Promise<boolean> {
133
+ public static validateEpochWinner(epochData: RawChallenge): boolean {
138
134
  const preimage = new ChallengeSolution(epochData);
139
- return await this.verifySolution(preimage);
135
+ return this.verifySolution(preimage);
140
136
  }
141
137
 
142
138
  /**
143
139
  * Validate epoch winner from Preimage instance
144
140
  */
145
- public static async validateChallengeSolution(challenge: IChallengeSolution): Promise<boolean> {
146
- return await this.verifySolution(challenge);
141
+ public static validateChallengeSolution(challenge: IChallengeSolution): boolean {
142
+ return this.verifySolution(challenge);
147
143
  }
148
144
 
149
145
  /**
@@ -153,13 +149,13 @@ export class EpochValidator {
153
149
  * @param salt The salt buffer (32 bytes)
154
150
  * @returns The SHA-1 hash of the preimage
155
151
  */
156
- public static async calculateSolution(
152
+ public static calculateSolution(
157
153
  targetChecksum: Buffer,
158
154
  publicKey: Buffer,
159
155
  salt: Buffer,
160
- ): Promise<Buffer> {
156
+ ): Buffer {
161
157
  const preimage = this.calculatePreimage(targetChecksum, publicKey, salt);
162
- const hash = await this.sha1(this.bufferToUint8Array(preimage));
158
+ const hash = this.sha1(this.bufferToUint8Array(preimage));
163
159
  return this.uint8ArrayToBuffer(hash);
164
160
  }
165
161