@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.
- package/browser/_version.d.ts +1 -1
- package/browser/epoch/ChallengeSolution.d.ts +3 -3
- package/browser/epoch/validator/EpochValidator.d.ts +5 -6
- package/browser/generators/builders/P2WDAGenerator.d.ts +13 -0
- package/browser/index.js +1 -1
- package/browser/keypair/Address.d.ts +3 -2
- package/browser/keypair/AddressVerificator.d.ts +13 -1
- package/browser/keypair/Wallet.d.ts +3 -0
- package/browser/opnet.d.ts +4 -0
- package/browser/p2wda/P2WDADetector.d.ts +16 -0
- package/browser/transaction/TransactionFactory.d.ts +3 -1
- package/browser/transaction/builders/DeploymentTransaction.d.ts +3 -3
- package/browser/transaction/builders/InteractionTransactionP2WDA.d.ts +37 -0
- package/browser/transaction/builders/SharedInteractionTransaction.d.ts +4 -4
- package/browser/transaction/builders/TransactionBuilder.d.ts +3 -0
- package/browser/transaction/interfaces/ITransactionParameters.d.ts +1 -0
- package/browser/transaction/mineable/IP2WSHAddress.d.ts +4 -0
- package/browser/transaction/mineable/TimelockGenerator.d.ts +2 -5
- package/browser/transaction/shared/TweakedTransaction.d.ts +23 -0
- package/build/_version.d.ts +1 -1
- package/build/_version.js +1 -1
- package/build/epoch/ChallengeSolution.d.ts +3 -3
- package/build/epoch/ChallengeSolution.js +3 -3
- package/build/epoch/validator/EpochValidator.d.ts +5 -6
- package/build/epoch/validator/EpochValidator.js +11 -12
- package/build/generators/builders/P2WDAGenerator.d.ts +13 -0
- package/build/generators/builders/P2WDAGenerator.js +62 -0
- package/build/keypair/Address.d.ts +3 -2
- package/build/keypair/Address.js +28 -2
- package/build/keypair/AddressVerificator.d.ts +13 -1
- package/build/keypair/AddressVerificator.js +82 -1
- package/build/keypair/Wallet.d.ts +3 -0
- package/build/keypair/Wallet.js +4 -0
- package/build/opnet.d.ts +4 -0
- package/build/opnet.js +4 -0
- package/build/p2wda/P2WDADetector.d.ts +16 -0
- package/build/p2wda/P2WDADetector.js +97 -0
- package/build/transaction/TransactionFactory.d.ts +3 -1
- package/build/transaction/TransactionFactory.js +35 -4
- package/build/transaction/builders/DeploymentTransaction.d.ts +3 -3
- package/build/transaction/builders/DeploymentTransaction.js +1 -1
- package/build/transaction/builders/InteractionTransactionP2WDA.d.ts +37 -0
- package/build/transaction/builders/InteractionTransactionP2WDA.js +205 -0
- package/build/transaction/builders/SharedInteractionTransaction.d.ts +4 -4
- package/build/transaction/builders/SharedInteractionTransaction.js +3 -3
- package/build/transaction/builders/TransactionBuilder.d.ts +3 -0
- package/build/transaction/builders/TransactionBuilder.js +18 -3
- package/build/transaction/interfaces/ITransactionParameters.d.ts +1 -0
- package/build/transaction/mineable/IP2WSHAddress.d.ts +4 -0
- package/build/transaction/mineable/IP2WSHAddress.js +1 -0
- package/build/transaction/mineable/TimelockGenerator.d.ts +2 -5
- package/build/transaction/shared/TweakedTransaction.d.ts +23 -0
- package/build/transaction/shared/TweakedTransaction.js +154 -18
- package/doc/README.md +0 -0
- package/doc/addresses/P2OP.md +1 -0
- package/doc/addresses/P2WDA.md +240 -0
- package/package.json +2 -2
- package/src/_version.ts +1 -1
- package/src/epoch/ChallengeSolution.ts +4 -4
- package/src/epoch/validator/EpochValidator.ts +12 -16
- package/src/generators/builders/P2WDAGenerator.ts +174 -0
- package/src/keypair/Address.ts +58 -3
- package/src/keypair/AddressVerificator.ts +147 -2
- package/src/keypair/Wallet.ts +16 -0
- package/src/opnet.ts +4 -0
- package/src/p2wda/P2WDADetector.ts +218 -0
- package/src/transaction/TransactionFactory.ts +79 -5
- package/src/transaction/builders/DeploymentTransaction.ts +4 -3
- package/src/transaction/builders/InteractionTransactionP2WDA.ts +376 -0
- package/src/transaction/builders/SharedInteractionTransaction.ts +7 -6
- package/src/transaction/builders/TransactionBuilder.ts +30 -3
- package/src/transaction/interfaces/ITransactionParameters.ts +1 -0
- package/src/transaction/mineable/IP2WSHAddress.ts +4 -0
- package/src/transaction/mineable/TimelockGenerator.ts +2 -6
- 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
|
|
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(
|
|
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(
|
|
401
|
+
else if (isP2WPKH(scriptPub) || isUnknownSegwitVersion(scriptPub)) {
|
|
359
402
|
}
|
|
360
|
-
else if (isP2WSHScript(
|
|
361
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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.
|
|
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.
|
|
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
|
+
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
|
|
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 {
|
|
119
|
+
* @returns {boolean} True if the challenge is valid
|
|
120
120
|
*/
|
|
121
|
-
public
|
|
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
|
|
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
|
|
26
|
-
|
|
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
|
|
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 =
|
|
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
|
|
133
|
+
public static validateEpochWinner(epochData: RawChallenge): boolean {
|
|
138
134
|
const preimage = new ChallengeSolution(epochData);
|
|
139
|
-
return
|
|
135
|
+
return this.verifySolution(preimage);
|
|
140
136
|
}
|
|
141
137
|
|
|
142
138
|
/**
|
|
143
139
|
* Validate epoch winner from Preimage instance
|
|
144
140
|
*/
|
|
145
|
-
public static
|
|
146
|
-
return
|
|
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
|
|
152
|
+
public static calculateSolution(
|
|
157
153
|
targetChecksum: Buffer,
|
|
158
154
|
publicKey: Buffer,
|
|
159
155
|
salt: Buffer,
|
|
160
|
-
):
|
|
156
|
+
): Buffer {
|
|
161
157
|
const preimage = this.calculatePreimage(targetChecksum, publicKey, salt);
|
|
162
|
-
const hash =
|
|
158
|
+
const hash = this.sha1(this.bufferToUint8Array(preimage));
|
|
163
159
|
return this.uint8ArrayToBuffer(hash);
|
|
164
160
|
}
|
|
165
161
|
|