@btc-vision/transaction 1.7.19 → 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.
- package/LICENSE +190 -21
- package/README.md +1 -1
- package/browser/_version.d.ts +1 -1
- package/browser/generators/builders/HashCommitmentGenerator.d.ts +49 -0
- package/browser/index.js +1 -1
- package/browser/keypair/Address.d.ts +3 -1
- package/browser/opnet.d.ts +6 -1
- package/browser/signer/AddressRotation.d.ts +12 -0
- package/browser/transaction/TransactionFactory.d.ts +14 -0
- package/browser/transaction/builders/ConsolidatedInteractionTransaction.d.ts +44 -0
- package/browser/transaction/enums/TransactionType.d.ts +3 -1
- package/browser/transaction/interfaces/IConsolidatedTransactionParameters.d.ts +31 -0
- package/browser/transaction/interfaces/ITransactionParameters.d.ts +2 -0
- package/browser/transaction/offline/OfflineTransactionManager.d.ts +69 -0
- package/browser/transaction/offline/TransactionReconstructor.d.ts +28 -0
- package/browser/transaction/offline/TransactionSerializer.d.ts +50 -0
- package/browser/transaction/offline/TransactionStateCapture.d.ts +52 -0
- package/browser/transaction/offline/index.d.ts +5 -0
- package/browser/transaction/offline/interfaces/ISerializableState.d.ts +62 -0
- package/browser/transaction/offline/interfaces/ITypeSpecificData.d.ts +62 -0
- package/browser/transaction/offline/interfaces/index.d.ts +2 -0
- package/browser/transaction/shared/TweakedTransaction.d.ts +12 -1
- package/browser/utxo/interfaces/IUTXO.d.ts +2 -0
- package/build/_version.d.ts +1 -1
- package/build/_version.js +1 -1
- package/build/generators/builders/HashCommitmentGenerator.d.ts +49 -0
- package/build/generators/builders/HashCommitmentGenerator.js +229 -0
- package/build/keypair/Address.d.ts +3 -1
- package/build/keypair/Address.js +87 -54
- package/build/opnet.d.ts +6 -1
- package/build/opnet.js +6 -1
- package/build/signer/AddressRotation.d.ts +12 -0
- package/build/signer/AddressRotation.js +16 -0
- package/build/transaction/TransactionFactory.d.ts +14 -0
- package/build/transaction/TransactionFactory.js +36 -0
- package/build/transaction/builders/ConsolidatedInteractionTransaction.d.ts +44 -0
- package/build/transaction/builders/ConsolidatedInteractionTransaction.js +259 -0
- package/build/transaction/builders/TransactionBuilder.js +2 -0
- package/build/transaction/enums/TransactionType.d.ts +3 -1
- package/build/transaction/enums/TransactionType.js +2 -0
- package/build/transaction/interfaces/IConsolidatedTransactionParameters.d.ts +31 -0
- package/build/transaction/interfaces/IConsolidatedTransactionParameters.js +1 -0
- package/build/transaction/interfaces/ITransactionParameters.d.ts +2 -0
- package/build/transaction/offline/OfflineTransactionManager.d.ts +69 -0
- package/build/transaction/offline/OfflineTransactionManager.js +255 -0
- package/build/transaction/offline/TransactionReconstructor.d.ts +28 -0
- package/build/transaction/offline/TransactionReconstructor.js +243 -0
- package/build/transaction/offline/TransactionSerializer.d.ts +50 -0
- package/build/transaction/offline/TransactionSerializer.js +700 -0
- package/build/transaction/offline/TransactionStateCapture.d.ts +52 -0
- package/build/transaction/offline/TransactionStateCapture.js +275 -0
- package/build/transaction/offline/index.d.ts +5 -0
- package/build/transaction/offline/index.js +5 -0
- package/build/transaction/offline/interfaces/ISerializableState.d.ts +62 -0
- package/build/transaction/offline/interfaces/ISerializableState.js +2 -0
- package/build/transaction/offline/interfaces/ITypeSpecificData.d.ts +62 -0
- package/build/transaction/offline/interfaces/ITypeSpecificData.js +19 -0
- package/build/transaction/offline/interfaces/index.d.ts +2 -0
- package/build/transaction/offline/interfaces/index.js +2 -0
- package/build/transaction/shared/TweakedTransaction.d.ts +12 -1
- package/build/transaction/shared/TweakedTransaction.js +75 -8
- package/build/utxo/interfaces/IUTXO.d.ts +2 -0
- package/documentation/README.md +5 -0
- package/documentation/offline-transaction-signing.md +650 -0
- package/documentation/transaction-building.md +603 -0
- package/package.json +2 -2
- package/src/_version.ts +1 -1
- package/src/generators/builders/HashCommitmentGenerator.ts +495 -0
- package/src/keypair/Address.ts +123 -70
- package/src/opnet.ts +8 -1
- package/src/signer/AddressRotation.ts +72 -0
- package/src/transaction/TransactionFactory.ts +90 -0
- package/src/transaction/builders/CancelTransaction.ts +4 -2
- package/src/transaction/builders/ConsolidatedInteractionTransaction.ts +568 -0
- package/src/transaction/builders/CustomScriptTransaction.ts +4 -2
- package/src/transaction/builders/MultiSignTransaction.ts +4 -2
- package/src/transaction/builders/TransactionBuilder.ts +8 -2
- package/src/transaction/enums/TransactionType.ts +2 -0
- package/src/transaction/interfaces/IConsolidatedTransactionParameters.ts +78 -0
- package/src/transaction/interfaces/ITransactionParameters.ts +8 -0
- package/src/transaction/offline/OfflineTransactionManager.ts +630 -0
- package/src/transaction/offline/TransactionReconstructor.ts +402 -0
- package/src/transaction/offline/TransactionSerializer.ts +920 -0
- package/src/transaction/offline/TransactionStateCapture.ts +469 -0
- package/src/transaction/offline/index.ts +8 -0
- package/src/transaction/offline/interfaces/ISerializableState.ts +141 -0
- package/src/transaction/offline/interfaces/ITypeSpecificData.ts +172 -0
- package/src/transaction/offline/interfaces/index.ts +2 -0
- package/src/transaction/shared/TweakedTransaction.ts +156 -9
- package/src/utxo/interfaces/IUTXO.ts +8 -0
- package/test/address-rotation.test.ts +553 -0
- package/test/offline-transaction.test.ts +2065 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
import { crypto, Network, networks, opcodes, payments, script } from '@btc-vision/bitcoin';
|
|
2
|
+
import { IHashCommittedP2WSH } from '../../transaction/interfaces/IConsolidatedTransactionParameters.js';
|
|
3
|
+
import { IP2WSHAddress } from '../../transaction/mineable/IP2WSHAddress.js';
|
|
4
|
+
import { Logger } from '@btc-vision/logger';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generates hash-committed P2WSH addresses for the Consolidated Hash-Committed Transaction (CHCT) system.
|
|
8
|
+
*
|
|
9
|
+
* These P2WSH scripts enforce that specific data must be provided in the witness to spend the output.
|
|
10
|
+
* If data is stripped or modified, the transaction fails at Bitcoin consensus level.
|
|
11
|
+
*
|
|
12
|
+
* Witness Script Structure (58 bytes):
|
|
13
|
+
* OP_HASH160 <20-byte-hash> OP_EQUALVERIFY <33-byte-pubkey> OP_CHECKSIG
|
|
14
|
+
*
|
|
15
|
+
* Witness Stack (when spending):
|
|
16
|
+
* [signature, data_chunk, witnessScript]
|
|
17
|
+
*/
|
|
18
|
+
export class HashCommitmentGenerator extends Logger {
|
|
19
|
+
/**
|
|
20
|
+
* Maximum chunk size per Bitcoin P2WSH stack item limit.
|
|
21
|
+
* See policy.h: MAX_STANDARD_P2WSH_STACK_ITEM_SIZE = 80
|
|
22
|
+
*/
|
|
23
|
+
public static readonly MAX_CHUNK_SIZE: number = 80;
|
|
24
|
+
/**
|
|
25
|
+
* Maximum stack items per P2WSH input.
|
|
26
|
+
* See policy.h: MAX_STANDARD_P2WSH_STACK_ITEMS = 100
|
|
27
|
+
*/
|
|
28
|
+
public static readonly MAX_STACK_ITEMS: number = 100;
|
|
29
|
+
/**
|
|
30
|
+
* Maximum total witness size (serialized).
|
|
31
|
+
* See policy.cpp: GetSerializeSize(tx.vin[i].scriptWitness.stack) > g_script_size_policy_limit
|
|
32
|
+
* Default: 1650 bytes
|
|
33
|
+
*/
|
|
34
|
+
public static readonly MAX_WITNESS_SIZE: number = 1650;
|
|
35
|
+
|
|
36
|
+
/** Maximum weight per standard transaction */
|
|
37
|
+
public static readonly MAX_STANDARD_WEIGHT: number = 400000;
|
|
38
|
+
/** Minimum satoshis per output (dust limit) */
|
|
39
|
+
public static readonly MIN_OUTPUT_VALUE: bigint = 330n;
|
|
40
|
+
/**
|
|
41
|
+
* Bytes per hash commitment in witness script.
|
|
42
|
+
* OP_HASH160 (1) + push (1) + hash (20) + OP_EQUALVERIFY (1) = 23 bytes
|
|
43
|
+
*/
|
|
44
|
+
private static readonly BYTES_PER_COMMITMENT: number = 23;
|
|
45
|
+
/**
|
|
46
|
+
* Signature check bytes in witness script.
|
|
47
|
+
* push (1) + pubkey (33) + OP_CHECKSIG (1) = 35 bytes
|
|
48
|
+
*/
|
|
49
|
+
private static readonly SIG_CHECK_BYTES: number = 35;
|
|
50
|
+
/**
|
|
51
|
+
* Fixed overhead in witness serialization:
|
|
52
|
+
* - Stack item count: 1 byte
|
|
53
|
+
* - Signature: 73 bytes (72 + 1 length prefix)
|
|
54
|
+
* - Script length prefix: 3 bytes (varInt for sizes 253-65535)
|
|
55
|
+
* - Script base (pubkey + checksig): 35 bytes
|
|
56
|
+
*/
|
|
57
|
+
private static readonly WITNESS_FIXED_OVERHEAD: number = 1 + 73 + 3 + 35;
|
|
58
|
+
/**
|
|
59
|
+
* Per-chunk overhead in witness:
|
|
60
|
+
* - Data: 81 bytes (80 + 1 length prefix)
|
|
61
|
+
* - Script commitment: 23 bytes
|
|
62
|
+
* Total: 104 bytes per chunk
|
|
63
|
+
*/
|
|
64
|
+
private static readonly WITNESS_PER_CHUNK_OVERHEAD: number =
|
|
65
|
+
HashCommitmentGenerator.MAX_CHUNK_SIZE + 1 + HashCommitmentGenerator.BYTES_PER_COMMITMENT;
|
|
66
|
+
/**
|
|
67
|
+
* Maximum data chunks per P2WSH output.
|
|
68
|
+
* Limited by total witness size: (1650 - 112) / 104 = 14 chunks
|
|
69
|
+
*/
|
|
70
|
+
public static readonly MAX_CHUNKS_PER_OUTPUT: number = Math.floor(
|
|
71
|
+
(HashCommitmentGenerator.MAX_WITNESS_SIZE -
|
|
72
|
+
HashCommitmentGenerator.WITNESS_FIXED_OVERHEAD) /
|
|
73
|
+
HashCommitmentGenerator.WITNESS_PER_CHUNK_OVERHEAD,
|
|
74
|
+
);
|
|
75
|
+
/** Base weight per input (non-witness): 41 bytes * 4 = 164 */
|
|
76
|
+
private static readonly INPUT_BASE_WEIGHT: number = 164;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Witness weight per input with max chunks:
|
|
80
|
+
* Total witness size is ~1566 bytes (under 1650 limit)
|
|
81
|
+
* Witness bytes count as 1 weight unit each.
|
|
82
|
+
*/
|
|
83
|
+
private static readonly INPUT_WITNESS_WEIGHT_MAX: number =
|
|
84
|
+
HashCommitmentGenerator.MAX_WITNESS_SIZE; // Use max as upper bound
|
|
85
|
+
|
|
86
|
+
/** Total weight per input (with max chunks) */
|
|
87
|
+
public static readonly WEIGHT_PER_INPUT: number =
|
|
88
|
+
HashCommitmentGenerator.INPUT_BASE_WEIGHT +
|
|
89
|
+
HashCommitmentGenerator.INPUT_WITNESS_WEIGHT_MAX;
|
|
90
|
+
public readonly logColor: string = '#4a90d9';
|
|
91
|
+
private readonly publicKey: Buffer;
|
|
92
|
+
private readonly network: Network;
|
|
93
|
+
|
|
94
|
+
constructor(publicKey: Buffer, network: Network = networks.bitcoin) {
|
|
95
|
+
super();
|
|
96
|
+
|
|
97
|
+
if (publicKey.length !== 33) {
|
|
98
|
+
throw new Error('Public key must be 33 bytes (compressed)');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.publicKey = publicKey;
|
|
102
|
+
this.network = network;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Calculate the maximum number of inputs per standard reveal transaction.
|
|
107
|
+
*
|
|
108
|
+
* Standard tx weight limit: 400,000
|
|
109
|
+
* With max chunks per input (~10,385 weight), only ~38 inputs fit
|
|
110
|
+
*
|
|
111
|
+
* @returns Maximum inputs per reveal tx (~38 with max chunks)
|
|
112
|
+
*/
|
|
113
|
+
public static calculateMaxInputsPerTx(): number {
|
|
114
|
+
const txOverhead = 40; // version, locktime, input/output counts
|
|
115
|
+
const outputOverhead = 200; // typical outputs (contract, change)
|
|
116
|
+
const availableWeight =
|
|
117
|
+
HashCommitmentGenerator.MAX_STANDARD_WEIGHT - txOverhead - outputOverhead;
|
|
118
|
+
|
|
119
|
+
return Math.floor(availableWeight / HashCommitmentGenerator.WEIGHT_PER_INPUT);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Calculate maximum data per standard reveal transaction.
|
|
124
|
+
*
|
|
125
|
+
* @returns Maximum data in bytes (~300KB with batched chunks at 70 chunks/output)
|
|
126
|
+
*/
|
|
127
|
+
public static calculateMaxDataPerTx(): number {
|
|
128
|
+
return (
|
|
129
|
+
HashCommitmentGenerator.calculateMaxInputsPerTx() *
|
|
130
|
+
HashCommitmentGenerator.MAX_CHUNKS_PER_OUTPUT *
|
|
131
|
+
HashCommitmentGenerator.MAX_CHUNK_SIZE
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Estimate the number of P2WSH outputs needed for a given data size.
|
|
137
|
+
*
|
|
138
|
+
* @param dataSize Data size in bytes
|
|
139
|
+
* @returns Number of P2WSH outputs needed
|
|
140
|
+
*/
|
|
141
|
+
public static estimateOutputCount(dataSize: number): number {
|
|
142
|
+
return Math.ceil(
|
|
143
|
+
dataSize /
|
|
144
|
+
(HashCommitmentGenerator.MAX_CHUNKS_PER_OUTPUT *
|
|
145
|
+
HashCommitmentGenerator.MAX_CHUNK_SIZE),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Estimate the number of 80-byte chunks for a given data size.
|
|
151
|
+
*
|
|
152
|
+
* @param dataSize Data size in bytes
|
|
153
|
+
* @returns Number of 80-byte chunks needed
|
|
154
|
+
*/
|
|
155
|
+
public static estimateChunkCount(dataSize: number): number {
|
|
156
|
+
return Math.ceil(dataSize / HashCommitmentGenerator.MAX_CHUNK_SIZE);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Validate that a witness script is a valid multi-hash committed script.
|
|
161
|
+
*
|
|
162
|
+
* Script structure: (OP_HASH160 <hash> OP_EQUALVERIFY)+ <pubkey> OP_CHECKSIG
|
|
163
|
+
*
|
|
164
|
+
* @param witnessScript The witness script to validate
|
|
165
|
+
* @returns true if valid hash-committed script
|
|
166
|
+
*/
|
|
167
|
+
public static validateHashCommittedScript(witnessScript: Buffer): boolean {
|
|
168
|
+
try {
|
|
169
|
+
const decompiled = script.decompile(witnessScript);
|
|
170
|
+
if (!decompiled || decompiled.length < 5) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Last two elements must be pubkey and OP_CHECKSIG
|
|
175
|
+
const lastIdx = decompiled.length - 1;
|
|
176
|
+
if (decompiled[lastIdx] !== opcodes.OP_CHECKSIG) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const pubkey = decompiled[lastIdx - 1];
|
|
180
|
+
if (!Buffer.isBuffer(pubkey) || pubkey.length !== 33) {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Everything before must be (OP_HASH160 <hash> OP_EQUALVERIFY) triplets
|
|
185
|
+
const hashParts = decompiled.slice(0, -2);
|
|
186
|
+
if (hashParts.length % 3 !== 0 || hashParts.length === 0) {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
for (let i = 0; i < hashParts.length; i += 3) {
|
|
191
|
+
const hash = hashParts[i + 1];
|
|
192
|
+
if (
|
|
193
|
+
hashParts[i] !== opcodes.OP_HASH160 ||
|
|
194
|
+
!Buffer.isBuffer(hash) ||
|
|
195
|
+
hash.length !== 20 ||
|
|
196
|
+
hashParts[i + 2] !== opcodes.OP_EQUALVERIFY
|
|
197
|
+
) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return true;
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Extract all data hashes from a hash-committed witness script.
|
|
210
|
+
*
|
|
211
|
+
* @param witnessScript The witness script
|
|
212
|
+
* @returns Array of 20-byte data hashes (in data order), or null if invalid
|
|
213
|
+
*/
|
|
214
|
+
public static extractDataHashes(witnessScript: Buffer): Buffer[] | null {
|
|
215
|
+
try {
|
|
216
|
+
const decompiled = script.decompile(witnessScript);
|
|
217
|
+
if (
|
|
218
|
+
!decompiled ||
|
|
219
|
+
!HashCommitmentGenerator.validateHashCommittedScript(witnessScript)
|
|
220
|
+
) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Extract hashes from triplets (they're in reverse order in script)
|
|
225
|
+
const hashParts = decompiled.slice(0, -2);
|
|
226
|
+
const hashes: Buffer[] = [];
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < hashParts.length; i += 3) {
|
|
229
|
+
hashes.push(hashParts[i + 1] as Buffer);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Reverse to get data order (script has them reversed)
|
|
233
|
+
return hashes.reverse();
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Extract the public key from a hash-committed witness script.
|
|
241
|
+
*
|
|
242
|
+
* @param witnessScript The witness script
|
|
243
|
+
* @returns The 33-byte public key, or null if invalid script
|
|
244
|
+
*/
|
|
245
|
+
public static extractPublicKey(witnessScript: Buffer): Buffer | null {
|
|
246
|
+
try {
|
|
247
|
+
const decompiled = script.decompile(witnessScript);
|
|
248
|
+
if (
|
|
249
|
+
!decompiled ||
|
|
250
|
+
!HashCommitmentGenerator.validateHashCommittedScript(witnessScript)
|
|
251
|
+
) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
return decompiled[decompiled.length - 2] as Buffer;
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Verify that data chunks match their committed hashes.
|
|
262
|
+
*
|
|
263
|
+
* @param dataChunks Array of data chunks (in order)
|
|
264
|
+
* @param witnessScript The witness script containing the hash commitments
|
|
265
|
+
* @returns true if all chunks match their commitments
|
|
266
|
+
*/
|
|
267
|
+
public static verifyChunkCommitments(dataChunks: Buffer[], witnessScript: Buffer): boolean {
|
|
268
|
+
const committedHashes = HashCommitmentGenerator.extractDataHashes(witnessScript);
|
|
269
|
+
if (!committedHashes || committedHashes.length !== dataChunks.length) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (let i = 0; i < dataChunks.length; i++) {
|
|
274
|
+
const actualHash = crypto.hash160(dataChunks[i]);
|
|
275
|
+
if (!committedHashes[i].equals(actualHash)) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Estimate fees for a complete CHCT flow (setup + reveal).
|
|
285
|
+
*
|
|
286
|
+
* @param dataSize Data size in bytes (before compression)
|
|
287
|
+
* @param feeRate Fee rate in sat/vB
|
|
288
|
+
* @param compressionRatio Expected compression ratio (default: 0.7)
|
|
289
|
+
* @returns Fee estimates
|
|
290
|
+
*/
|
|
291
|
+
public static estimateFees(
|
|
292
|
+
dataSize: number,
|
|
293
|
+
feeRate: number,
|
|
294
|
+
compressionRatio: number = 0.7,
|
|
295
|
+
): {
|
|
296
|
+
compressedSize: number;
|
|
297
|
+
outputCount: number;
|
|
298
|
+
chunkCount: number;
|
|
299
|
+
setupVBytes: number;
|
|
300
|
+
revealVBytes: number;
|
|
301
|
+
setupFee: bigint;
|
|
302
|
+
revealFee: bigint;
|
|
303
|
+
totalFee: bigint;
|
|
304
|
+
outputsValue: bigint;
|
|
305
|
+
totalCost: bigint;
|
|
306
|
+
} {
|
|
307
|
+
const compressedSize = Math.ceil(dataSize * compressionRatio);
|
|
308
|
+
const outputCount = HashCommitmentGenerator.estimateOutputCount(compressedSize);
|
|
309
|
+
const chunkCount = HashCommitmentGenerator.estimateChunkCount(compressedSize);
|
|
310
|
+
|
|
311
|
+
// Setup tx: inputs (funding) + outputs (P2WSH commitments + change)
|
|
312
|
+
// Estimate: 2 P2TR inputs + N P2WSH outputs + 1 change output
|
|
313
|
+
const setupInputVBytes = 2 * 58; // P2TR inputs ~58 vB each
|
|
314
|
+
const setupOutputVBytes = outputCount * 43 + 43; // P2WSH outputs ~43 vB, change ~43 vB
|
|
315
|
+
const setupOverhead = 11; // version, locktime, counts
|
|
316
|
+
const setupVBytes = setupOverhead + setupInputVBytes + setupOutputVBytes;
|
|
317
|
+
|
|
318
|
+
// Reveal tx: N P2WSH inputs (each with up to 98 data chunks) + contract output + change
|
|
319
|
+
const revealWeight = 40 + outputCount * HashCommitmentGenerator.WEIGHT_PER_INPUT + 200;
|
|
320
|
+
const revealVBytes = Math.ceil(revealWeight / 4);
|
|
321
|
+
|
|
322
|
+
const setupFee = BigInt(Math.ceil(setupVBytes * feeRate));
|
|
323
|
+
const revealFee = BigInt(Math.ceil(revealVBytes * feeRate));
|
|
324
|
+
const totalFee = setupFee + revealFee;
|
|
325
|
+
|
|
326
|
+
const outputsValue = BigInt(outputCount) * HashCommitmentGenerator.MIN_OUTPUT_VALUE;
|
|
327
|
+
const totalCost = totalFee + outputsValue;
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
compressedSize,
|
|
331
|
+
outputCount,
|
|
332
|
+
chunkCount,
|
|
333
|
+
setupVBytes,
|
|
334
|
+
revealVBytes,
|
|
335
|
+
setupFee,
|
|
336
|
+
revealFee,
|
|
337
|
+
totalFee,
|
|
338
|
+
outputsValue,
|
|
339
|
+
totalCost,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Calculate the HASH160 of a data chunk.
|
|
345
|
+
* HASH160 = RIPEMD160(SHA256(data))
|
|
346
|
+
*/
|
|
347
|
+
public hashChunk(data: Buffer): Buffer {
|
|
348
|
+
return crypto.hash160(data);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Generate a hash-committed witness script for multiple data chunks.
|
|
353
|
+
*
|
|
354
|
+
* Script structure (for N chunks):
|
|
355
|
+
* OP_HASH160 <hash_N> OP_EQUALVERIFY
|
|
356
|
+
* OP_HASH160 <hash_N-1> OP_EQUALVERIFY
|
|
357
|
+
* ...
|
|
358
|
+
* OP_HASH160 <hash_1> OP_EQUALVERIFY
|
|
359
|
+
* <pubkey> OP_CHECKSIG
|
|
360
|
+
*
|
|
361
|
+
* Hashes are in reverse order because witness stack is LIFO.
|
|
362
|
+
* Witness stack: [sig, data_1, data_2, ..., data_N, witnessScript]
|
|
363
|
+
* Stack before execution: [sig, data_1, data_2, ..., data_N] (data_N on top)
|
|
364
|
+
*
|
|
365
|
+
* @param dataHashes Array of HASH160 values (in data order, will be reversed in script)
|
|
366
|
+
* @returns The compiled witness script
|
|
367
|
+
*/
|
|
368
|
+
public generateWitnessScript(dataHashes: Buffer[]): Buffer {
|
|
369
|
+
if (dataHashes.length === 0) {
|
|
370
|
+
throw new Error('At least one data hash is required');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (dataHashes.length > HashCommitmentGenerator.MAX_CHUNKS_PER_OUTPUT) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`Too many chunks: ${dataHashes.length} exceeds limit of ${HashCommitmentGenerator.MAX_CHUNKS_PER_OUTPUT}`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const hash of dataHashes) {
|
|
380
|
+
if (hash.length !== 20) {
|
|
381
|
+
throw new Error(`HASH160 requires 20-byte hash, got ${hash.length}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Build script parts - hashes in reverse order (last data chunk verified first)
|
|
386
|
+
const scriptParts: (number | Buffer)[] = [];
|
|
387
|
+
|
|
388
|
+
// Add hash commitments in reverse order
|
|
389
|
+
for (let i = dataHashes.length - 1; i >= 0; i--) {
|
|
390
|
+
scriptParts.push(opcodes.OP_HASH160);
|
|
391
|
+
scriptParts.push(dataHashes[i]);
|
|
392
|
+
scriptParts.push(opcodes.OP_EQUALVERIFY);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Add signature check
|
|
396
|
+
scriptParts.push(this.publicKey);
|
|
397
|
+
scriptParts.push(opcodes.OP_CHECKSIG);
|
|
398
|
+
|
|
399
|
+
return script.compile(scriptParts);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Generate a P2WSH address from a witness script.
|
|
404
|
+
*
|
|
405
|
+
* @param witnessScript The witness script
|
|
406
|
+
* @returns P2WSH address info
|
|
407
|
+
*/
|
|
408
|
+
public generateP2WSHAddress(witnessScript: Buffer): IP2WSHAddress & { scriptPubKey: Buffer } {
|
|
409
|
+
const p2wsh = payments.p2wsh({
|
|
410
|
+
redeem: { output: witnessScript },
|
|
411
|
+
network: this.network,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (!p2wsh.address || !p2wsh.output) {
|
|
415
|
+
throw new Error('Failed to generate P2WSH address');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
address: p2wsh.address,
|
|
420
|
+
witnessScript,
|
|
421
|
+
scriptPubKey: p2wsh.output,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Split data into chunks and generate hash-committed P2WSH outputs.
|
|
427
|
+
*
|
|
428
|
+
* Each output commits to up to 98 data chunks (80 bytes each = 7,840 bytes).
|
|
429
|
+
* This is MUCH more efficient than one output per chunk.
|
|
430
|
+
*
|
|
431
|
+
* @param data The data to chunk and commit
|
|
432
|
+
* @param maxChunkSize Maximum bytes per stack item (default: 80, P2WSH stack item limit)
|
|
433
|
+
* @returns Array of hash-committed P2WSH outputs
|
|
434
|
+
*/
|
|
435
|
+
public prepareChunks(
|
|
436
|
+
data: Buffer,
|
|
437
|
+
maxChunkSize: number = HashCommitmentGenerator.MAX_CHUNK_SIZE,
|
|
438
|
+
): IHashCommittedP2WSH[] {
|
|
439
|
+
if (maxChunkSize > HashCommitmentGenerator.MAX_CHUNK_SIZE) {
|
|
440
|
+
throw new Error(
|
|
441
|
+
`Chunk size ${maxChunkSize} exceeds P2WSH stack item limit of ${HashCommitmentGenerator.MAX_CHUNK_SIZE}`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (data.length === 0) {
|
|
446
|
+
throw new Error('Data cannot be empty');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// First, split data into 80-byte chunks
|
|
450
|
+
const allChunks: Buffer[] = [];
|
|
451
|
+
let offset = 0;
|
|
452
|
+
|
|
453
|
+
while (offset < data.length) {
|
|
454
|
+
const chunkSize = Math.min(maxChunkSize, data.length - offset);
|
|
455
|
+
allChunks.push(Buffer.from(data.subarray(offset, offset + chunkSize)));
|
|
456
|
+
offset += chunkSize;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Now batch chunks into outputs (up to 98 chunks per output)
|
|
460
|
+
const outputs: IHashCommittedP2WSH[] = [];
|
|
461
|
+
let chunkIndex = 0;
|
|
462
|
+
|
|
463
|
+
while (chunkIndex < allChunks.length) {
|
|
464
|
+
const chunksForThisOutput = allChunks.slice(
|
|
465
|
+
chunkIndex,
|
|
466
|
+
chunkIndex + HashCommitmentGenerator.MAX_CHUNKS_PER_OUTPUT,
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const dataChunks = chunksForThisOutput;
|
|
470
|
+
const dataHashes = dataChunks.map((chunk) => this.hashChunk(chunk));
|
|
471
|
+
|
|
472
|
+
const witnessScript = this.generateWitnessScript(dataHashes);
|
|
473
|
+
const p2wsh = this.generateP2WSHAddress(witnessScript);
|
|
474
|
+
|
|
475
|
+
outputs.push({
|
|
476
|
+
address: p2wsh.address,
|
|
477
|
+
witnessScript: p2wsh.witnessScript,
|
|
478
|
+
scriptPubKey: p2wsh.scriptPubKey,
|
|
479
|
+
dataHashes,
|
|
480
|
+
dataChunks,
|
|
481
|
+
chunkStartIndex: chunkIndex,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
chunkIndex += chunksForThisOutput.length;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const totalChunks = allChunks.length;
|
|
488
|
+
this.log(
|
|
489
|
+
`Prepared ${outputs.length} P2WSH outputs with ${totalChunks} chunks ` +
|
|
490
|
+
`(${data.length} bytes, ~${Math.ceil(data.length / outputs.length)} bytes/output)`,
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
return outputs;
|
|
494
|
+
}
|
|
495
|
+
}
|