@btc-vision/transaction 1.7.18 → 1.7.22

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 (92) hide show
  1. package/LICENSE +190 -21
  2. package/README.md +1 -1
  3. package/browser/_version.d.ts +1 -1
  4. package/browser/generators/builders/HashCommitmentGenerator.d.ts +49 -0
  5. package/browser/index.js +1 -1
  6. package/browser/keypair/Address.d.ts +3 -1
  7. package/browser/opnet.d.ts +6 -1
  8. package/browser/signer/AddressRotation.d.ts +12 -0
  9. package/browser/transaction/TransactionFactory.d.ts +14 -0
  10. package/browser/transaction/builders/ConsolidatedInteractionTransaction.d.ts +44 -0
  11. package/browser/transaction/enums/TransactionType.d.ts +3 -1
  12. package/browser/transaction/interfaces/IConsolidatedTransactionParameters.d.ts +31 -0
  13. package/browser/transaction/interfaces/ITransactionParameters.d.ts +2 -0
  14. package/browser/transaction/offline/OfflineTransactionManager.d.ts +69 -0
  15. package/browser/transaction/offline/TransactionReconstructor.d.ts +28 -0
  16. package/browser/transaction/offline/TransactionSerializer.d.ts +50 -0
  17. package/browser/transaction/offline/TransactionStateCapture.d.ts +52 -0
  18. package/browser/transaction/offline/index.d.ts +5 -0
  19. package/browser/transaction/offline/interfaces/ISerializableState.d.ts +62 -0
  20. package/browser/transaction/offline/interfaces/ITypeSpecificData.d.ts +62 -0
  21. package/browser/transaction/offline/interfaces/index.d.ts +2 -0
  22. package/browser/transaction/shared/TweakedTransaction.d.ts +12 -1
  23. package/browser/utxo/interfaces/IUTXO.d.ts +2 -0
  24. package/build/_version.d.ts +1 -1
  25. package/build/_version.js +1 -1
  26. package/build/generators/builders/HashCommitmentGenerator.d.ts +49 -0
  27. package/build/generators/builders/HashCommitmentGenerator.js +229 -0
  28. package/build/keypair/Address.d.ts +3 -1
  29. package/build/keypair/Address.js +87 -54
  30. package/build/opnet.d.ts +6 -1
  31. package/build/opnet.js +6 -1
  32. package/build/signer/AddressRotation.d.ts +12 -0
  33. package/build/signer/AddressRotation.js +16 -0
  34. package/build/transaction/TransactionFactory.d.ts +14 -0
  35. package/build/transaction/TransactionFactory.js +36 -0
  36. package/build/transaction/builders/ConsolidatedInteractionTransaction.d.ts +44 -0
  37. package/build/transaction/builders/ConsolidatedInteractionTransaction.js +259 -0
  38. package/build/transaction/builders/TransactionBuilder.js +2 -0
  39. package/build/transaction/enums/TransactionType.d.ts +3 -1
  40. package/build/transaction/enums/TransactionType.js +2 -0
  41. package/build/transaction/interfaces/IConsolidatedTransactionParameters.d.ts +31 -0
  42. package/build/transaction/interfaces/IConsolidatedTransactionParameters.js +1 -0
  43. package/build/transaction/interfaces/ITransactionParameters.d.ts +2 -0
  44. package/build/transaction/offline/OfflineTransactionManager.d.ts +69 -0
  45. package/build/transaction/offline/OfflineTransactionManager.js +255 -0
  46. package/build/transaction/offline/TransactionReconstructor.d.ts +28 -0
  47. package/build/transaction/offline/TransactionReconstructor.js +243 -0
  48. package/build/transaction/offline/TransactionSerializer.d.ts +50 -0
  49. package/build/transaction/offline/TransactionSerializer.js +700 -0
  50. package/build/transaction/offline/TransactionStateCapture.d.ts +52 -0
  51. package/build/transaction/offline/TransactionStateCapture.js +275 -0
  52. package/build/transaction/offline/index.d.ts +5 -0
  53. package/build/transaction/offline/index.js +5 -0
  54. package/build/transaction/offline/interfaces/ISerializableState.d.ts +62 -0
  55. package/build/transaction/offline/interfaces/ISerializableState.js +2 -0
  56. package/build/transaction/offline/interfaces/ITypeSpecificData.d.ts +62 -0
  57. package/build/transaction/offline/interfaces/ITypeSpecificData.js +19 -0
  58. package/build/transaction/offline/interfaces/index.d.ts +2 -0
  59. package/build/transaction/offline/interfaces/index.js +2 -0
  60. package/build/transaction/shared/TweakedTransaction.d.ts +12 -1
  61. package/build/transaction/shared/TweakedTransaction.js +75 -8
  62. package/build/utxo/interfaces/IUTXO.d.ts +2 -0
  63. package/documentation/README.md +5 -0
  64. package/documentation/offline-transaction-signing.md +650 -0
  65. package/documentation/transaction-building.md +603 -0
  66. package/package.json +2 -2
  67. package/src/_version.ts +1 -1
  68. package/src/generators/builders/HashCommitmentGenerator.ts +495 -0
  69. package/src/keypair/Address.ts +123 -70
  70. package/src/opnet.ts +8 -1
  71. package/src/signer/AddressRotation.ts +72 -0
  72. package/src/transaction/TransactionFactory.ts +94 -1
  73. package/src/transaction/builders/CancelTransaction.ts +4 -2
  74. package/src/transaction/builders/ConsolidatedInteractionTransaction.ts +568 -0
  75. package/src/transaction/builders/CustomScriptTransaction.ts +4 -2
  76. package/src/transaction/builders/MultiSignTransaction.ts +4 -2
  77. package/src/transaction/builders/TransactionBuilder.ts +8 -2
  78. package/src/transaction/enums/TransactionType.ts +2 -0
  79. package/src/transaction/interfaces/IConsolidatedTransactionParameters.ts +78 -0
  80. package/src/transaction/interfaces/ITransactionParameters.ts +8 -0
  81. package/src/transaction/offline/OfflineTransactionManager.ts +630 -0
  82. package/src/transaction/offline/TransactionReconstructor.ts +402 -0
  83. package/src/transaction/offline/TransactionSerializer.ts +920 -0
  84. package/src/transaction/offline/TransactionStateCapture.ts +469 -0
  85. package/src/transaction/offline/index.ts +8 -0
  86. package/src/transaction/offline/interfaces/ISerializableState.ts +141 -0
  87. package/src/transaction/offline/interfaces/ITypeSpecificData.ts +172 -0
  88. package/src/transaction/offline/interfaces/index.ts +2 -0
  89. package/src/transaction/shared/TweakedTransaction.ts +156 -9
  90. package/src/utxo/interfaces/IUTXO.ts +8 -0
  91. package/test/address-rotation.test.ts +553 -0
  92. package/test/offline-transaction.test.ts +2065 -0
@@ -0,0 +1,568 @@
1
+ import { Buffer } from 'buffer';
2
+ import { Psbt, PsbtInput, Transaction, toXOnly } from '@btc-vision/bitcoin';
3
+ import { ECPairInterface } from 'ecpair';
4
+ import { TransactionType } from '../enums/TransactionType.js';
5
+ import { MINIMUM_AMOUNT_REWARD, TransactionBuilder } from './TransactionBuilder.js';
6
+ import { HashCommitmentGenerator } from '../../generators/builders/HashCommitmentGenerator.js';
7
+ import { CalldataGenerator } from '../../generators/builders/CalldataGenerator.js';
8
+ import {
9
+ IConsolidatedInteractionParameters,
10
+ IConsolidatedInteractionResult,
11
+ IHashCommittedP2WSH,
12
+ IRevealTransactionResult,
13
+ ISetupTransactionResult,
14
+ } from '../interfaces/IConsolidatedTransactionParameters.js';
15
+ import { IP2WSHAddress } from '../mineable/IP2WSHAddress.js';
16
+ import { TimeLockGenerator } from '../mineable/TimelockGenerator.js';
17
+ import { ChallengeSolution } from '../../epoch/ChallengeSolution.js';
18
+ import { EcKeyPair } from '../../keypair/EcKeyPair.js';
19
+ import { BitcoinUtils } from '../../utils/BitcoinUtils.js';
20
+ import { Compressor } from '../../bytecode/Compressor.js';
21
+ import { Feature, FeaturePriority, Features } from '../../generators/Features.js';
22
+ import { AddressGenerator } from '../../generators/AddressGenerator.js';
23
+
24
+ /**
25
+ * Consolidated Interaction Transaction
26
+ *
27
+ * Drop-in replacement for InteractionTransaction that bypasses BIP110/Bitcoin Knots censorship.
28
+ *
29
+ * Uses the same parameters and sends the same data on-chain as InteractionTransaction,
30
+ * but embeds data in hash-committed P2WSH witnesses instead of Tapscript.
31
+ *
32
+ * Data is split into 80-byte chunks (P2WSH stack item limit), with up to 14 chunks
33
+ * batched per P2WSH output (~1,120 bytes per output). Each output's witness script
34
+ * commits to all its chunks via HASH160. When spent, all data chunks are revealed
35
+ * in the witness and verified at consensus level.
36
+ *
37
+ * Policy limits respected:
38
+ * - MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80 bytes per chunk
39
+ * - g_script_size_policy_limit = 1650 bytes total witness size (serialized)
40
+ * - MAX_STANDARD_P2WSH_STACK_ITEMS = 100 items per witness
41
+ *
42
+ * Data integrity is consensus-enforced: if any data is stripped or modified,
43
+ * HASH160(data) != committed_hash and the transaction is INVALID.
44
+ *
45
+ * Capacity: ~1.1KB per P2WSH output, ~220 outputs per reveal tx, ~242KB max.
46
+ *
47
+ * Usage:
48
+ * ```typescript
49
+ * // Same parameters as InteractionTransaction
50
+ * const tx = new ConsolidatedInteractionTransaction({
51
+ * calldata: myCalldata,
52
+ * to: contractAddress,
53
+ * contract: contractSecret,
54
+ * challenge: myChallenge,
55
+ * utxos: myUtxos,
56
+ * signer: mySigner,
57
+ * network: networks.bitcoin,
58
+ * feeRate: 10,
59
+ * priorityFee: 0n,
60
+ * gasSatFee: 330n,
61
+ * });
62
+ *
63
+ * const result = await tx.build();
64
+ * // Broadcast setup first, then reveal (can use CPFP)
65
+ * broadcast(result.setup.txHex);
66
+ * broadcast(result.reveal.txHex);
67
+ * ```
68
+ */
69
+ export class ConsolidatedInteractionTransaction extends TransactionBuilder<TransactionType.INTERACTION> {
70
+ public readonly type: TransactionType.INTERACTION = TransactionType.INTERACTION;
71
+
72
+ /** The contract address (same as InteractionTransaction.to) */
73
+ protected readonly contractAddress: string;
74
+
75
+ /** The contract secret - 32 bytes (same as InteractionTransaction) */
76
+ protected readonly contractSecret: Buffer;
77
+
78
+ /** The compressed calldata (same as InteractionTransaction) */
79
+ protected readonly calldata: Buffer;
80
+
81
+ /** Challenge solution for epoch (same as InteractionTransaction) */
82
+ protected readonly challenge: ChallengeSolution;
83
+
84
+ /** Epoch challenge P2WSH address (same as InteractionTransaction) */
85
+ protected readonly epochChallenge: IP2WSHAddress;
86
+
87
+ /** Random bytes for interaction (same as InteractionTransaction) */
88
+ public readonly randomBytes: Buffer;
89
+
90
+ /** Script signer for interaction (same as InteractionTransaction) */
91
+ protected readonly scriptSigner: ECPairInterface;
92
+
93
+ /** Calldata generator - produces same output as InteractionTransaction */
94
+ protected readonly calldataGenerator: CalldataGenerator;
95
+
96
+ /** Hash commitment generator for CHCT */
97
+ protected readonly hashCommitmentGenerator: HashCommitmentGenerator;
98
+
99
+ /** The compiled operation data - SAME as InteractionTransaction's compiledTargetScript */
100
+ protected readonly compiledTargetScript: Buffer;
101
+
102
+ /** Generated hash-committed P2WSH outputs */
103
+ protected readonly commitmentOutputs: IHashCommittedP2WSH[];
104
+
105
+ /** Disable auto refund (same as InteractionTransaction) */
106
+ protected readonly disableAutoRefund: boolean;
107
+
108
+ /** Maximum chunk size (default: 80 bytes per P2WSH stack item limit) */
109
+ protected readonly maxChunkSize: number;
110
+
111
+ /** Cached value per output (calculated once, used by setup and reveal) */
112
+ private cachedValuePerOutput: bigint | null = null;
113
+
114
+ constructor(parameters: IConsolidatedInteractionParameters) {
115
+ super(parameters);
116
+
117
+ // Same validation as InteractionTransaction
118
+ if (!parameters.to) {
119
+ throw new Error('Contract address (to) is required');
120
+ }
121
+
122
+ if (!parameters.contract) {
123
+ throw new Error('Contract secret (contract) is required');
124
+ }
125
+
126
+ if (!parameters.calldata) {
127
+ throw new Error('Calldata is required');
128
+ }
129
+
130
+ if (!parameters.challenge) {
131
+ throw new Error('Challenge solution is required');
132
+ }
133
+
134
+ this.contractAddress = parameters.to;
135
+ this.contractSecret = Buffer.from(parameters.contract.replace('0x', ''), 'hex');
136
+ this.disableAutoRefund = parameters.disableAutoRefund || false;
137
+ this.maxChunkSize = parameters.maxChunkSize ?? HashCommitmentGenerator.MAX_CHUNK_SIZE;
138
+
139
+ // Validate contract secret (same as InteractionTransaction)
140
+ if (this.contractSecret.length !== 32) {
141
+ throw new Error('Invalid contract secret length. Expected 32 bytes.');
142
+ }
143
+
144
+ // Compress calldata (same as SharedInteractionTransaction)
145
+ this.calldata = Compressor.compress(parameters.calldata);
146
+
147
+ // Generate random bytes and script signer (same as SharedInteractionTransaction)
148
+ this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes();
149
+ this.scriptSigner = EcKeyPair.fromSeedKeyPair(this.randomBytes, this.network);
150
+
151
+ // Generate epoch challenge address (same as SharedInteractionTransaction)
152
+ this.challenge = parameters.challenge;
153
+ this.epochChallenge = TimeLockGenerator.generateTimeLockAddress(
154
+ this.challenge.publicKey.originalPublicKeyBuffer(),
155
+ this.network,
156
+ );
157
+
158
+ // Create calldata generator (same as SharedInteractionTransaction)
159
+ this.calldataGenerator = new CalldataGenerator(
160
+ Buffer.from(this.signer.publicKey),
161
+ toXOnly(Buffer.from(this.scriptSigner.publicKey)),
162
+ this.network,
163
+ );
164
+
165
+ // Compile the target script - SAME as InteractionTransaction
166
+ if (parameters.compiledTargetScript) {
167
+ if (Buffer.isBuffer(parameters.compiledTargetScript)) {
168
+ this.compiledTargetScript = parameters.compiledTargetScript;
169
+ } else if (typeof parameters.compiledTargetScript === 'string') {
170
+ this.compiledTargetScript = Buffer.from(parameters.compiledTargetScript, 'hex');
171
+ } else {
172
+ throw new Error('Invalid compiled target script format.');
173
+ }
174
+ } else {
175
+ this.compiledTargetScript = this.calldataGenerator.compile(
176
+ this.calldata,
177
+ this.contractSecret,
178
+ this.challenge,
179
+ this.priorityFee,
180
+ this.generateFeatures(parameters),
181
+ );
182
+ }
183
+
184
+ // Create hash commitment generator
185
+ this.hashCommitmentGenerator = new HashCommitmentGenerator(
186
+ Buffer.from(this.signer.publicKey),
187
+ this.network,
188
+ );
189
+
190
+ // Split compiled data into hash-committed chunks
191
+ this.commitmentOutputs = this.hashCommitmentGenerator.prepareChunks(
192
+ this.compiledTargetScript,
193
+ this.maxChunkSize,
194
+ );
195
+
196
+ // Validate output count
197
+ this.validateOutputCount();
198
+
199
+ const totalChunks = this.commitmentOutputs.reduce(
200
+ (sum, output) => sum + output.dataChunks.length,
201
+ 0,
202
+ );
203
+ this.log(
204
+ `ConsolidatedInteractionTransaction: ${this.commitmentOutputs.length} outputs, ` +
205
+ `${totalChunks} chunks from ${this.compiledTargetScript.length} bytes compiled data`,
206
+ );
207
+
208
+ this.internalInit();
209
+ }
210
+
211
+ /**
212
+ * Get the compiled target script (same as InteractionTransaction).
213
+ */
214
+ public exportCompiledTargetScript(): Buffer {
215
+ return this.compiledTargetScript;
216
+ }
217
+
218
+ /**
219
+ * Get the contract secret (same as InteractionTransaction).
220
+ */
221
+ public getContractSecret(): Buffer {
222
+ return this.contractSecret;
223
+ }
224
+
225
+ /**
226
+ * Get the random bytes (same as InteractionTransaction).
227
+ */
228
+ public getRndBytes(): Buffer {
229
+ return this.randomBytes;
230
+ }
231
+
232
+ /**
233
+ * Get the challenge solution (same as InteractionTransaction).
234
+ */
235
+ public getChallenge(): ChallengeSolution {
236
+ return this.challenge;
237
+ }
238
+
239
+ /**
240
+ * Get the commitment outputs for the setup transaction.
241
+ */
242
+ public getCommitmentOutputs(): IHashCommittedP2WSH[] {
243
+ return this.commitmentOutputs;
244
+ }
245
+
246
+ /**
247
+ * Get the number of P2WSH outputs.
248
+ */
249
+ public getOutputCount(): number {
250
+ return this.commitmentOutputs.length;
251
+ }
252
+
253
+ /**
254
+ * Get the total number of 80-byte chunks across all outputs.
255
+ */
256
+ public getTotalChunkCount(): number {
257
+ return this.commitmentOutputs.reduce((sum, output) => sum + output.dataChunks.length, 0);
258
+ }
259
+
260
+ /**
261
+ * Build both setup and reveal transactions.
262
+ *
263
+ * @returns Complete result with both transactions
264
+ */
265
+ public async build(): Promise<IConsolidatedInteractionResult> {
266
+ // Build and sign setup transaction using base class flow
267
+ const setupTx = await this.signTransaction();
268
+ const setupTxId = setupTx.getId();
269
+
270
+ const setup: ISetupTransactionResult = {
271
+ txHex: setupTx.toHex(),
272
+ txId: setupTxId,
273
+ outputs: this.commitmentOutputs,
274
+ feesPaid: this.transactionFee,
275
+ chunkCount: this.getTotalChunkCount(),
276
+ totalDataSize: this.compiledTargetScript.length,
277
+ };
278
+
279
+ this.log(`Setup transaction: ${setup.txId}`);
280
+
281
+ // Build reveal transaction
282
+ const reveal = this.buildRevealTransaction(setupTxId);
283
+
284
+ return {
285
+ setup,
286
+ reveal,
287
+ totalFees: setup.feesPaid + reveal.feesPaid,
288
+ };
289
+ }
290
+
291
+ /**
292
+ * Build the setup transaction.
293
+ * Creates P2WSH outputs with hash commitments to the compiled data chunks.
294
+ * This is called by signTransaction() in the base class.
295
+ */
296
+ protected override async buildTransaction(): Promise<void> {
297
+ // Add funding UTXOs as inputs
298
+ this.addInputsFromUTXO();
299
+
300
+ // Calculate value per output (includes reveal fee + OPNet fee)
301
+ const valuePerOutput = this.calculateValuePerOutput();
302
+
303
+ // Add each hash-committed P2WSH as an output
304
+ for (const commitment of this.commitmentOutputs) {
305
+ this.addOutput({
306
+ value: Number(valuePerOutput),
307
+ address: commitment.address,
308
+ });
309
+ }
310
+
311
+ // Calculate total spent on commitment outputs
312
+ const totalCommitmentValue = BigInt(this.commitmentOutputs.length) * valuePerOutput;
313
+
314
+ // Add optional outputs
315
+ const optionalAmount = this.addOptionalOutputsAndGetAmount();
316
+
317
+ // Add refund/change output
318
+ await this.addRefundOutput(totalCommitmentValue + optionalAmount);
319
+ }
320
+
321
+ /**
322
+ * Build the reveal transaction.
323
+ * Spends the P2WSH commitment outputs, revealing the compiled data in witnesses.
324
+ *
325
+ * Output structure matches InteractionTransaction:
326
+ * - Output to epochChallenge.address (miner reward)
327
+ * - Change output (if any)
328
+ *
329
+ * @param setupTxId The transaction ID of the setup transaction
330
+ */
331
+ public buildRevealTransaction(setupTxId: string): IRevealTransactionResult {
332
+ const revealPsbt = new Psbt({ network: this.network });
333
+
334
+ // Get the value per output (same as used in setup transaction)
335
+ const valuePerOutput = this.calculateValuePerOutput();
336
+
337
+ // Add commitment outputs as inputs (from setup tx)
338
+ for (let i = 0; i < this.commitmentOutputs.length; i++) {
339
+ const commitment = this.commitmentOutputs[i];
340
+
341
+ revealPsbt.addInput({
342
+ hash: setupTxId,
343
+ index: i,
344
+ witnessUtxo: {
345
+ script: commitment.scriptPubKey,
346
+ value: Number(valuePerOutput),
347
+ },
348
+ witnessScript: commitment.witnessScript,
349
+ });
350
+ }
351
+
352
+ // Calculate input value from commitments
353
+ const inputValue = BigInt(this.commitmentOutputs.length) * valuePerOutput;
354
+
355
+ // Calculate OPNet fee (same as InteractionTransaction)
356
+ const opnetFee = this.getTransactionOPNetFee();
357
+ const feeAmount = opnetFee < MINIMUM_AMOUNT_REWARD ? MINIMUM_AMOUNT_REWARD : opnetFee;
358
+
359
+ // Add output to epoch challenge address (same as InteractionTransaction)
360
+ revealPsbt.addOutput({
361
+ address: this.epochChallenge.address,
362
+ value: Number(feeAmount),
363
+ });
364
+
365
+ // Estimate reveal transaction fee
366
+ const estimatedVBytes = this.estimateRevealVBytes();
367
+ const revealFee = BigInt(Math.ceil(estimatedVBytes * this.feeRate));
368
+
369
+ // Add change output if there's enough left
370
+ const changeValue = inputValue - feeAmount - revealFee;
371
+ if (changeValue > TransactionBuilder.MINIMUM_DUST) {
372
+ const refundAddress = this.getRefundAddress();
373
+ revealPsbt.addOutput({
374
+ address: refundAddress,
375
+ value: Number(changeValue),
376
+ });
377
+ }
378
+
379
+ // Sign all commitment inputs
380
+ for (let i = 0; i < this.commitmentOutputs.length; i++) {
381
+ revealPsbt.signInput(i, this.signer);
382
+ }
383
+
384
+ // Finalize all inputs with hash-commitment finalizer
385
+ for (let i = 0; i < this.commitmentOutputs.length; i++) {
386
+ const commitment = this.commitmentOutputs[i];
387
+ revealPsbt.finalizeInput(i, (_inputIndex: number, input: PsbtInput) => {
388
+ return this.finalizeCommitmentInput(input, commitment);
389
+ });
390
+ }
391
+
392
+ const revealTx: Transaction = revealPsbt.extractTransaction();
393
+
394
+ const result: IRevealTransactionResult = {
395
+ txHex: revealTx.toHex(),
396
+ txId: revealTx.getId(),
397
+ dataSize: this.compiledTargetScript.length,
398
+ feesPaid: revealFee,
399
+ inputCount: this.commitmentOutputs.length,
400
+ };
401
+
402
+ this.log(`Reveal transaction: ${result.txId}`);
403
+
404
+ return result;
405
+ }
406
+
407
+ /**
408
+ * Finalize a commitment input.
409
+ *
410
+ * Witness stack: [signature, data_1, data_2, ..., data_N, witnessScript]
411
+ *
412
+ * The witness script verifies each data chunk against its committed hash.
413
+ * If any data is wrong or missing, the transaction is INVALID at consensus level.
414
+ */
415
+ private finalizeCommitmentInput(
416
+ input: PsbtInput,
417
+ commitment: IHashCommittedP2WSH,
418
+ ): {
419
+ finalScriptSig: Buffer | undefined;
420
+ finalScriptWitness: Buffer | undefined;
421
+ } {
422
+ if (!input.partialSig || input.partialSig.length === 0) {
423
+ throw new Error('No signature for commitment input');
424
+ }
425
+
426
+ if (!input.witnessScript) {
427
+ throw new Error('No witness script for commitment input');
428
+ }
429
+
430
+ // Witness stack for hash-committed P2WSH with multiple chunks
431
+ // Order: [signature, data_1, data_2, ..., data_N, witnessScript]
432
+ const witnessStack: Buffer[] = [
433
+ input.partialSig[0].signature, // Signature for OP_CHECKSIG
434
+ ...commitment.dataChunks, // All data chunks for OP_HASH160 verification
435
+ input.witnessScript, // The witness script
436
+ ];
437
+
438
+ return {
439
+ finalScriptSig: undefined,
440
+ finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witnessStack),
441
+ };
442
+ }
443
+
444
+ /**
445
+ * Estimate reveal transaction vBytes.
446
+ */
447
+ private estimateRevealVBytes(): number {
448
+ const inputCount = this.commitmentOutputs.length;
449
+
450
+ // Calculate actual witness weight based on chunks per output
451
+ let witnessWeight = 0;
452
+ for (const commitment of this.commitmentOutputs) {
453
+ // Per input: 41 bytes base (× 4) + witness data
454
+ // Witness: signature (~72) + chunks (N × 80) + script (N × 23 + 35) + overhead (~20)
455
+ const numChunks = commitment.dataChunks.length;
456
+ const chunkDataWeight = numChunks * 80; // actual data
457
+ const scriptWeight = numChunks * 23 + 35; // witness script
458
+ const sigWeight = 72;
459
+ const overheadWeight = 20;
460
+
461
+ witnessWeight += 164 + chunkDataWeight + scriptWeight + sigWeight + overheadWeight;
462
+ }
463
+
464
+ const weight = 40 + witnessWeight + 200; // tx overhead + witnesses + outputs
465
+ return Math.ceil(weight / 4);
466
+ }
467
+
468
+ /**
469
+ * Calculate the required value per commitment output.
470
+ * This must cover: dust minimum + share of reveal fee + share of OPNet fee
471
+ */
472
+ private calculateValuePerOutput(): bigint {
473
+ // Return cached value if already calculated
474
+ if (this.cachedValuePerOutput !== null) {
475
+ return this.cachedValuePerOutput;
476
+ }
477
+
478
+ const numOutputs = this.commitmentOutputs.length;
479
+
480
+ // Calculate OPNet fee
481
+ const opnetFee = this.getTransactionOPNetFee();
482
+ const feeAmount = opnetFee < MINIMUM_AMOUNT_REWARD ? MINIMUM_AMOUNT_REWARD : opnetFee;
483
+
484
+ // Calculate reveal fee
485
+ const estimatedVBytes = this.estimateRevealVBytes();
486
+ const revealFee = BigInt(Math.ceil(estimatedVBytes * this.feeRate));
487
+
488
+ // Total needed: OPNet fee + reveal fee + dust for change
489
+ const totalNeeded = feeAmount + revealFee + TransactionBuilder.MINIMUM_DUST;
490
+
491
+ // Distribute across outputs, ensuring at least MIN_OUTPUT_VALUE per output
492
+ const valuePerOutput = BigInt(Math.ceil(Number(totalNeeded) / numOutputs));
493
+ const minValue = HashCommitmentGenerator.MIN_OUTPUT_VALUE;
494
+
495
+ this.cachedValuePerOutput = valuePerOutput > minValue ? valuePerOutput : minValue;
496
+ return this.cachedValuePerOutput;
497
+ }
498
+
499
+ /**
500
+ * Get the value per commitment output (for external access).
501
+ */
502
+ public getValuePerOutput(): bigint {
503
+ return this.calculateValuePerOutput();
504
+ }
505
+
506
+ /**
507
+ * Get refund address.
508
+ */
509
+ private getRefundAddress(): string {
510
+ if (this.from) {
511
+ return this.from;
512
+ }
513
+
514
+ return AddressGenerator.generatePKSH(this.signer.publicKey, this.network);
515
+ }
516
+
517
+ /**
518
+ * Generate features (same as InteractionTransaction).
519
+ */
520
+ private generateFeatures(parameters: IConsolidatedInteractionParameters): Feature<Features>[] {
521
+ const features: Feature<Features>[] = [];
522
+
523
+ if (parameters.loadedStorage) {
524
+ features.push({
525
+ priority: FeaturePriority.ACCESS_LIST,
526
+ opcode: Features.ACCESS_LIST,
527
+ data: parameters.loadedStorage,
528
+ });
529
+ }
530
+
531
+ const submission = parameters.challenge.getSubmission();
532
+ if (submission) {
533
+ features.push({
534
+ priority: FeaturePriority.EPOCH_SUBMISSION,
535
+ opcode: Features.EPOCH_SUBMISSION,
536
+ data: submission,
537
+ });
538
+ }
539
+
540
+ if (parameters.revealMLDSAPublicKey && !parameters.linkMLDSAPublicKeyToAddress) {
541
+ throw new Error(
542
+ 'To reveal the MLDSA public key, you must set linkMLDSAPublicKeyToAddress to true.',
543
+ );
544
+ }
545
+
546
+ if (parameters.linkMLDSAPublicKeyToAddress) {
547
+ this.generateMLDSALinkRequest(parameters, features);
548
+ }
549
+
550
+ return features;
551
+ }
552
+
553
+ /**
554
+ * Validate output count is within standard tx limits.
555
+ */
556
+ private validateOutputCount(): void {
557
+ const maxInputs = HashCommitmentGenerator.calculateMaxInputsPerTx();
558
+
559
+ if (this.commitmentOutputs.length > maxInputs) {
560
+ const maxData = HashCommitmentGenerator.calculateMaxDataPerTx();
561
+ throw new Error(
562
+ `Data too large: ${this.commitmentOutputs.length} P2WSH outputs needed, ` +
563
+ `max ${maxInputs} per standard transaction (~${Math.floor(maxData / 1024)}KB). ` +
564
+ `Compiled data: ${this.compiledTargetScript.length} bytes.`,
565
+ );
566
+ }
567
+ }
568
+ }
@@ -20,8 +20,10 @@ import { EcKeyPair } from '../../keypair/EcKeyPair.js';
20
20
  import { AddressGenerator } from '../../generators/AddressGenerator.js';
21
21
  import { ECPairInterface } from 'ecpair';
22
22
 
23
- export interface ICustomTransactionParameters
24
- extends Omit<SharedInteractionParameters, 'challenge'> {
23
+ export interface ICustomTransactionParameters extends Omit<
24
+ SharedInteractionParameters,
25
+ 'challenge'
26
+ > {
25
27
  script: (Buffer | Stack)[];
26
28
  witnesses: Buffer[];
27
29
 
@@ -21,8 +21,10 @@ import { UTXO } from '../../utxo/interfaces/IUTXO.js';
21
21
  import { EcKeyPair } from '../../keypair/EcKeyPair.js';
22
22
  import { ECPairInterface } from 'ecpair';
23
23
 
24
- export interface MultiSignParameters
25
- extends Omit<ITransactionParameters, 'gasSatFee' | 'priorityFee' | 'signer'> {
24
+ export interface MultiSignParameters extends Omit<
25
+ ITransactionParameters,
26
+ 'gasSatFee' | 'priorityFee' | 'signer'
27
+ > {
26
28
  readonly pubkeys: Buffer[];
27
29
  readonly minimumSignatures: number;
28
30
  readonly from?: undefined;
@@ -1081,8 +1081,11 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
1081
1081
 
1082
1082
  for (let i = 0; i < this.utxos.length; i++) {
1083
1083
  const utxo = this.utxos[i];
1084
- const input = this.generatePsbtInputExtended(utxo, i);
1085
1084
 
1085
+ // Register signer BEFORE generating input (needed for tapInternalKey)
1086
+ this.registerInputSigner(i, utxo);
1087
+
1088
+ const input = this.generatePsbtInputExtended(utxo, i);
1086
1089
  this.addInput(input);
1087
1090
  }
1088
1091
  }
@@ -1094,8 +1097,11 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
1094
1097
  i++
1095
1098
  ) {
1096
1099
  const utxo = this.optionalInputs[i - this.utxos.length];
1097
- const input = this.generatePsbtInputExtended(utxo, i, true);
1098
1100
 
1101
+ // Register signer BEFORE generating input (needed for tapInternalKey)
1102
+ this.registerInputSigner(i, utxo);
1103
+
1104
+ const input = this.generatePsbtInputExtended(utxo, i, true);
1099
1105
  this.addInput(input);
1100
1106
  }
1101
1107
  }
@@ -6,4 +6,6 @@ export enum TransactionType {
6
6
  MULTI_SIG = 4,
7
7
  CUSTOM_CODE = 5,
8
8
  CANCEL = 6,
9
+ CONSOLIDATED_SETUP = 7,
10
+ CONSOLIDATED_REVEAL = 8,
9
11
  }