@totemsdk/core 1.0.8 → 1.0.10

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 (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -5
  3. package/dist/Streamable.js +54 -31
  4. package/dist/adapters/index.js +18 -8
  5. package/dist/adapters/types.js +19 -8
  6. package/dist/binio.js +5 -1
  7. package/dist/bip39.js +24 -14
  8. package/dist/derive.js +11 -7
  9. package/dist/index.js +130 -22
  10. package/dist/javaStreamables.js +49 -30
  11. package/dist/lease/LeaseMonitor.js +7 -3
  12. package/dist/lease/LeaseStore.js +7 -3
  13. package/dist/lease/WatermarkStore.js +7 -3
  14. package/dist/lease/index.js +13 -4
  15. package/dist/lease-client.js +8 -3
  16. package/dist/minima32.js +22 -13
  17. package/dist/minimaWireSerializer.js +20 -14
  18. package/dist/mmr.js +35 -23
  19. package/dist/mx.js +10 -1
  20. package/dist/params.js +9 -5
  21. package/dist/script.js +12 -7
  22. package/dist/scripts/contract-helpers.js +37 -26
  23. package/dist/scripts/dapp-types.js +11 -6
  24. package/dist/scripts/index.js +38 -4
  25. package/dist/scripts/types.js +26 -12
  26. package/dist/scripts/witness-serializer.js +48 -30
  27. package/dist/transaction.js +55 -46
  28. package/dist/treekey.js +74 -58
  29. package/dist/tx/TransactionLifecycle.js +11 -5
  30. package/dist/tx/TransactionReceiptStore.js +7 -3
  31. package/dist/tx/TransactionService.js +14 -10
  32. package/dist/tx/index.js +11 -3
  33. package/dist/tx/types.js +2 -1
  34. package/dist/util.js +5 -1
  35. package/dist/utils.js +12 -5
  36. package/dist/verify.js +36 -25
  37. package/dist/version.js +5 -2
  38. package/dist/wots.js +94 -64
  39. package/package.json +35 -6
  40. package/src/Streamable.d.ts +0 -265
  41. package/src/Streamable.js +0 -431
  42. package/src/Streamable.ts +0 -567
  43. package/src/__tests__/Streamable.parity.test.ts +0 -422
  44. package/src/__tests__/perAddressDerivation.parity.test.ts +0 -152
  45. package/src/__tests__/roottree-prepare.test.ts +0 -101
  46. package/src/__tests__/wots-parity.test.ts +0 -289
  47. package/src/adapters/index.d.ts +0 -190
  48. package/src/adapters/index.js +0 -117
  49. package/src/adapters/index.ts +0 -301
  50. package/src/adapters/types.ts +0 -235
  51. package/src/addr.oracle.test.ts +0 -25
  52. package/src/address.test.ts +0 -55
  53. package/src/binio.ts +0 -21
  54. package/src/bip39.d.ts +0 -91
  55. package/src/bip39.js +0 -427
  56. package/src/bip39.ts +0 -432
  57. package/src/derive.d.ts +0 -2
  58. package/src/derive.js +0 -13
  59. package/src/derive.ts +0 -11
  60. package/src/index.d.ts +0 -26
  61. package/src/index.js +0 -128
  62. package/src/index.test.ts +0 -5
  63. package/src/index.ts +0 -221
  64. package/src/javaStreamables.d.ts +0 -233
  65. package/src/javaStreamables.js +0 -328
  66. package/src/javaStreamables.ts +0 -403
  67. package/src/lease/LeaseMonitor.d.ts +0 -44
  68. package/src/lease/LeaseMonitor.js +0 -111
  69. package/src/lease/LeaseMonitor.ts +0 -157
  70. package/src/lease/LeaseStore.d.ts +0 -57
  71. package/src/lease/LeaseStore.js +0 -148
  72. package/src/lease/LeaseStore.ts +0 -203
  73. package/src/lease/WatermarkStore.d.ts +0 -61
  74. package/src/lease/WatermarkStore.js +0 -206
  75. package/src/lease/WatermarkStore.ts +0 -270
  76. package/src/lease/index.d.ts +0 -14
  77. package/src/lease/index.js +0 -22
  78. package/src/lease/index.ts +0 -41
  79. package/src/lease-client.d.ts +0 -31
  80. package/src/lease-client.js +0 -44
  81. package/src/lease-client.ts +0 -69
  82. package/src/minima32.d.ts +0 -10
  83. package/src/minima32.js +0 -113
  84. package/src/minima32.test.ts +0 -11
  85. package/src/minima32.ts +0 -127
  86. package/src/minimaWireSerializer.d.ts +0 -189
  87. package/src/minimaWireSerializer.js +0 -311
  88. package/src/minimaWireSerializer.test.ts +0 -466
  89. package/src/minimaWireSerializer.ts +0 -117
  90. package/src/mmr.d.ts +0 -159
  91. package/src/mmr.js +0 -352
  92. package/src/mmr.oracle.test.ts +0 -28
  93. package/src/mmr.ts +0 -429
  94. package/src/mx.test.ts +0 -25
  95. package/src/mx.ts +0 -1
  96. package/src/params.d.ts +0 -29
  97. package/src/params.d.ts.map +0 -1
  98. package/src/params.js +0 -20
  99. package/src/params.js.map +0 -1
  100. package/src/params.ts +0 -44
  101. package/src/script.d.ts +0 -7
  102. package/src/script.js +0 -27
  103. package/src/script.ts +0 -24
  104. package/src/scripts/contract-helpers.ts +0 -609
  105. package/src/scripts/dapp-types.ts +0 -240
  106. package/src/scripts/index.ts +0 -22
  107. package/src/scripts/types.ts +0 -247
  108. package/src/scripts/witness-serializer.ts +0 -384
  109. package/src/transaction.ts +0 -430
  110. package/src/treekey.d.ts +0 -383
  111. package/src/treekey.js +0 -774
  112. package/src/treekey.parity.test.ts +0 -459
  113. package/src/treekey.ts +0 -975
  114. package/src/tx/TransactionLifecycle.d.ts +0 -55
  115. package/src/tx/TransactionLifecycle.js +0 -188
  116. package/src/tx/TransactionLifecycle.ts +0 -280
  117. package/src/tx/TransactionReceiptStore.d.ts +0 -36
  118. package/src/tx/TransactionReceiptStore.js +0 -89
  119. package/src/tx/TransactionReceiptStore.ts +0 -112
  120. package/src/tx/TransactionService.d.ts +0 -31
  121. package/src/tx/TransactionService.js +0 -140
  122. package/src/tx/TransactionService.ts +0 -252
  123. package/src/tx/index.d.ts +0 -13
  124. package/src/tx/index.js +0 -20
  125. package/src/tx/index.ts +0 -44
  126. package/src/tx/types.d.ts +0 -80
  127. package/src/tx/types.js +0 -6
  128. package/src/tx/types.ts +0 -130
  129. package/src/util.d.ts +0 -1
  130. package/src/util.js +0 -5
  131. package/src/util.ts +0 -1
  132. package/src/utils.d.ts +0 -8
  133. package/src/utils.js +0 -42
  134. package/src/utils.ts +0 -38
  135. package/src/verify.ts +0 -210
  136. package/src/version.ts +0 -13
  137. package/src/wots.d.ts +0 -224
  138. package/src/wots.d.ts.map +0 -1
  139. package/src/wots.js +0 -565
  140. package/src/wots.js.map +0 -1
  141. package/src/wots.ts +0 -651
  142. package/test-vectors.json +0 -45
package/src/wots.ts DELETED
@@ -1,651 +0,0 @@
1
- /**
2
- * WOTS Implementation - BouncyCastle Compatible
3
- *
4
- * This implementation matches BouncyCastle's WinternitzOTSignature exactly,
5
- * which is used by Minima's Winternitz.java class.
6
- *
7
- * Key differences from standard WOTS/WOTS+:
8
- * - w=8 means 8 BITS per digit (not base-8), so each byte is one digit (0-255)
9
- * - Stateful PRNG (GMSSRandom) for chain seed derivation, not stateless H(i||seed)
10
- * - 34 total chains (32 message + 2 checksum), not 89
11
- * - Each chain hashed up to 255 times (2^8 - 1)
12
- *
13
- * Source: org.bouncycastle.pqc.crypto.gmss.util.{WinternitzOTSignature, WinternitzOTSVerify, GMSSRandom}
14
- */
15
-
16
- import { sha3_256 } from '@noble/hashes/sha3.js';
17
- import { getParamSet, ParamSet } from "./params.js";
18
- import { deriveChainSeedJava } from "./javaStreamables.js";
19
- import type { LoggerAdapter } from "./adapters/index.js";
20
- import { NoopLogger } from "./adapters/index.js";
21
-
22
- // Module-level debug logger - can be set externally for parity testing
23
- let wotsLogger: LoggerAdapter = new NoopLogger();
24
- let wotsDebugEnabled = false;
25
-
26
- /**
27
- * Enable WOTS debug logging with a custom logger
28
- * Use this for parity testing to capture all intermediate values
29
- *
30
- * WARNING: Debug logging outputs sensitive cryptographic material including
31
- * private key chains and seeds. NEVER enable in production builds.
32
- * This is intended for development/testing parity verification only.
33
- */
34
- export function setWotsLogger(logger: LoggerAdapter): void {
35
- wotsLogger = logger;
36
- wotsDebugEnabled = true;
37
- }
38
-
39
- /**
40
- * Disable WOTS debug logging
41
- */
42
- export function disableWotsLogger(): void {
43
- wotsLogger = new NoopLogger();
44
- wotsDebugEnabled = false;
45
- }
46
-
47
- /**
48
- * Check if WOTS debug logging is enabled
49
- */
50
- export function isWotsDebugEnabled(): boolean {
51
- return wotsDebugEnabled;
52
- }
53
-
54
- // === Canonical helpers ===
55
- export const F = (x: Uint8Array) => sha3_256(x);
56
- export const hex = (u: Uint8Array): string => {
57
- let s = '';
58
- for (let i = 0; i < u.length; i++) s += u[i].toString(16).padStart(2, '0');
59
- return s;
60
- };
61
- export const fromHex = (h: string): Uint8Array => {
62
- const s = h.replace(/^0x/, '');
63
- const out = new Uint8Array(s.length >> 1);
64
- for (let i = 0; i < out.length; i++) out[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16);
65
- return out;
66
- };
67
- export const concatBytes = (...arrs: Uint8Array[]) => {
68
- const len = arrs.reduce((n, a) => n + a.length, 0);
69
- const out = new Uint8Array(len);
70
- let o = 0; for (const a of arrs) { out.set(a, o); o += a.length; }
71
- return out;
72
- };
73
- export const u16be = (n: number) => new Uint8Array([(n >>> 8) & 0xff, n & 0xff]);
74
- export const u32be = (n: number) => new Uint8Array([(n >>> 24)&255,(n>>>16)&255,(n>>>8)&255,n&255]);
75
- export function assert32(u: Uint8Array, label='value'){
76
- if (!(u instanceof Uint8Array)) throw new Error(`${label} must be a Uint8Array`);
77
- if (u.length !== 32) throw new Error(`${label} must be exactly 32 bytes; got ${u.length}`);
78
- }
79
-
80
- // Legacy aliases
81
- const u8 = (len: number) => new Uint8Array(len);
82
- export const h = F;
83
- const concat = concatBytes;
84
-
85
- const defaultParamSet = getParamSet();
86
-
87
- /**
88
- * GMSSRandom - BouncyCastle's stateful PRNG for WOTS chain seed derivation
89
- *
90
- * Algorithm from GMSSRandom.java:
91
- * rand = H(state)
92
- * state = state + rand + 1 (byte-wise addition with carry)
93
- * return rand
94
- *
95
- * The state is MUTATED in place after each call.
96
- */
97
- export class GMSSRandom {
98
- /**
99
- * Generate next random value and update state
100
- *
101
- * @param state - 32-byte state array (MUTATED in place)
102
- * @returns 32-byte random value
103
- */
104
- static nextSeed(state: Uint8Array): Uint8Array {
105
- assert32(state, 'state');
106
-
107
- // rand = H(state)
108
- const rand = F(state);
109
-
110
- // state = state + rand (byte-wise addition with carry, little-endian)
111
- GMSSRandom.addByteArrays(state, rand);
112
-
113
- // state = state + 1
114
- GMSSRandom.addOne(state);
115
-
116
- return rand;
117
- }
118
-
119
- /**
120
- * Add two byte arrays: a = a + b (little-endian, with carry)
121
- * Matches GMSSRandom.addByteArrays()
122
- */
123
- private static addByteArrays(a: Uint8Array, b: Uint8Array): void {
124
- let overflow = 0;
125
- for (let i = 0; i < a.length; i++) {
126
- const temp = (a[i] & 0xff) + (b[i] & 0xff) + overflow;
127
- a[i] = temp & 0xff;
128
- overflow = temp >>> 8;
129
- }
130
- }
131
-
132
- /**
133
- * Add one to byte array: a = a + 1 (little-endian, with carry)
134
- * Matches GMSSRandom.addOne()
135
- */
136
- private static addOne(a: Uint8Array): void {
137
- let overflow = 1;
138
- for (let i = 0; i < a.length; i++) {
139
- const temp = (a[i] & 0xff) + overflow;
140
- a[i] = temp & 0xff;
141
- overflow = temp >>> 8;
142
- if (overflow === 0) break;
143
- }
144
- }
145
- }
146
-
147
- /**
148
- * getLog - Calculate ceil(log2(x))
149
- *
150
- * Matches WinternitzOTSignature.getLog():
151
- * int log = 1;
152
- * int i = 2;
153
- * while (i < intValue) { i <<= 1; log++; }
154
- * return log;
155
- */
156
- export function getLog(intValue: number): number {
157
- let log = 1;
158
- let i = 2;
159
- while (i < intValue) {
160
- i <<= 1;
161
- log++;
162
- }
163
- return log;
164
- }
165
-
166
- /**
167
- * Expand master seed into L private key chains using GMSSRandom
168
- *
169
- * Matches WinternitzOTSignature constructor:
170
- * byte[] dummy = new byte[mdsize];
171
- * System.arraycopy(seed0, 0, dummy, 0, dummy.length);
172
- * for (int i = 0; i < keysize; i++) {
173
- * privateKeyOTS[i] = gmssRandom.nextSeed(dummy);
174
- * }
175
- */
176
- export function expandPrivateKey(seed: Uint8Array, ps: ParamSet = getParamSet()): Uint8Array[] {
177
- assert32(seed, 'seed');
178
-
179
- // Copy seed to mutable state
180
- const state = new Uint8Array(seed);
181
-
182
- if (wotsDebugEnabled) {
183
- wotsLogger.debug(`[expandPrivateKey] Input seed: ${hex(seed).substring(0, 64)}...`);
184
- wotsLogger.debug(`[expandPrivateKey] Initial state: ${hex(state).substring(0, 64)}...`);
185
- }
186
-
187
- // Generate L chain seeds using stateful PRNG
188
- const privateKeys: Uint8Array[] = [];
189
- for (let i = 0; i < ps.L; i++) {
190
- const pk = GMSSRandom.nextSeed(state);
191
- privateKeys.push(pk);
192
-
193
- // Log first few private keys for debugging
194
- if (wotsDebugEnabled && i < 3) {
195
- wotsLogger.debug(`[expandPrivateKey] pk[${i}]: ${hex(pk)}`);
196
- wotsLogger.debug(`[expandPrivateKey] state after pk[${i}]: ${hex(state).substring(0, 32)}...`);
197
- }
198
- }
199
-
200
- if (wotsDebugEnabled) {
201
- wotsLogger.debug(`[expandPrivateKey] Generated ${privateKeys.length} private keys`);
202
- wotsLogger.debug(`[expandPrivateKey] pk[33] (last): ${hex(privateKeys[33])}`);
203
- }
204
-
205
- return privateKeys;
206
- }
207
-
208
- /**
209
- * Convert message hash to Winternitz digits with checksum
210
- *
211
- * For w=8 (8 bits per digit), since 8 % 8 == 0:
212
- * - Each byte of the hash IS one digit (0-255)
213
- * - messagesize = 32 digits
214
- * - checksum = (messagesize << w) - sum = 8192 - sum
215
- * - checksumsize = 14 bits, extracted as 2 digits
216
- *
217
- * Matches WinternitzOTSignature.getSignature() for w=8 case
218
- */
219
- export function toWinternitzDigits(hash32: Uint8Array, ps: ParamSet = getParamSet()): {
220
- digits: number[],
221
- checksumDigits: number[],
222
- total: number
223
- } {
224
- assert32(hash32, 'hash');
225
-
226
- if (ps.w !== 8) {
227
- throw new Error(`Only w=8 is supported. Got w=${ps.w}`);
228
- }
229
-
230
- // For w=8: each byte IS a digit (0-255)
231
- // d = 8 / w = 1 digit per byte
232
- // k = (1 << w) - 1 = 255
233
- const digits: number[] = [];
234
- let c = 0; // checksum accumulator
235
-
236
- for (let i = 0; i < hash32.length; i++) {
237
- const digit = hash32[i] & 0xff;
238
- digits.push(digit);
239
- c += digit;
240
- }
241
-
242
- const digitSum = c;
243
-
244
- // Checksum = (messagesize << w) - c = (32 << 8) - c = 8192 - c
245
- c = (ps.messageSize << ps.w) - c;
246
-
247
- const checksumBeforeExtract = c;
248
-
249
- // Extract checksum digits: 2 digits, each 0-255
250
- // checksumsize = 14 bits, iterate by w=8 bits
251
- const checksumDigits: number[] = [];
252
- const k = ps.maxDigit; // 255
253
- for (let i = 0; i < ps.checksumSize; i += ps.w) {
254
- checksumDigits.push(c & k);
255
- c >>>= ps.w;
256
- }
257
-
258
- if (wotsDebugEnabled) {
259
- wotsLogger.debug(`[toWinternitzDigits] Input hash: ${hex(hash32)}`);
260
- wotsLogger.debug(`[toWinternitzDigits] First 5 message digits: [${digits.slice(0, 5).join(', ')}]`);
261
- wotsLogger.debug(`[toWinternitzDigits] Last 5 message digits: [${digits.slice(-5).join(', ')}]`);
262
- wotsLogger.debug(`[toWinternitzDigits] Digit sum: ${digitSum}`);
263
- wotsLogger.debug(`[toWinternitzDigits] Checksum formula: (${ps.messageSize} << ${ps.w}) - ${digitSum} = ${checksumBeforeExtract}`);
264
- wotsLogger.debug(`[toWinternitzDigits] Checksum digits: [${checksumDigits.join(', ')}]`);
265
- wotsLogger.debug(`[toWinternitzDigits] Total digits: ${digits.length + checksumDigits.length} (expected L=${ps.L})`);
266
- }
267
-
268
- return {
269
- digits,
270
- checksumDigits,
271
- total: digits.length + checksumDigits.length
272
- };
273
- }
274
-
275
- /**
276
- * Decompose digest into base-w digits + checksum
277
- * Returns flat array of all L digits
278
- */
279
- export function baseWWithChecksum(msgHash: Uint8Array, paramSet?: ParamSet): number[] {
280
- const ps = paramSet || defaultParamSet;
281
- const { digits, checksumDigits } = toWinternitzDigits(msgHash, ps);
282
- const result = digits.concat(checksumDigits);
283
-
284
- if (result.length !== ps.L) {
285
- throw new Error(`Invalid digit length: expected ${ps.L}, got ${result.length}`);
286
- }
287
-
288
- return result;
289
- }
290
-
291
- /**
292
- * Hash a value k times
293
- *
294
- * Matches WinternitzOTSignature.hashPrivateKeyBlock():
295
- * - If rounds < 1: return input unchanged
296
- * - Otherwise: hash `rounds` times
297
- */
298
- export function hashChain(x: Uint8Array, rounds: number): Uint8Array {
299
- if (rounds < 1) {
300
- return new Uint8Array(x);
301
- }
302
- let result = F(x);
303
- for (let i = 1; i < rounds; i++) {
304
- result = F(result);
305
- }
306
- return result;
307
- }
308
-
309
- export type WotsKeypair = {
310
- seed: Uint8Array;
311
- index: number;
312
- pk: Uint8Array;
313
- };
314
-
315
- /**
316
- * Derive unique seed for a specific key index
317
- *
318
- * DEPRECATED: Use deriveChainSeedJava for Java parity.
319
- * This function is preserved for backward compatibility only.
320
- *
321
- * The correct derivation matching Minima's TreeKeyNode.java:
322
- * MiniData seed = Crypto.getInstance().hashAllObjects(new MiniNumber(i), zPrivateSeed);
323
- *
324
- * This old implementation used H(seed || u32be(i)) which is NOT Java compatible.
325
- * @deprecated Use deriveChainSeedJava from javaStreamables.ts
326
- */
327
- export function deriveIndexedSeed(seed: Uint8Array, keyIndex: number): Uint8Array {
328
- assert32(seed, 'seed');
329
- // Use Java-compatible derivation: hashAllObjects(MiniNumber(i), MiniData(seed))
330
- const result = deriveChainSeedJava(seed, keyIndex);
331
-
332
- if (wotsDebugEnabled) {
333
- wotsLogger.debug(`[deriveIndexedSeed] seed=${hex(seed).substring(0, 32)}..., keyIndex=${keyIndex} => ${hex(result)}`);
334
- }
335
-
336
- return result;
337
- }
338
-
339
- /**
340
- * Derive WOTS public key digest from seed and key index
341
- *
342
- * The key index is mixed into the seed first (deriveIndexedSeed),
343
- * then expanded using GMSSRandom to get unique chain seeds.
344
- *
345
- * Matches WinternitzOTSignature.getPublicKey():
346
- * int rounds = (1 << w) - 1; // 255 for w=8
347
- * for (int i = 0; i < keysize; i++) {
348
- * hashPrivateKeyBlock(i, rounds, buf, pos);
349
- * }
350
- * return H(buf);
351
- */
352
- export function derivePKdigest(seed: Uint8Array, i: number, ps: ParamSet = getParamSet()): Uint8Array {
353
- assert32(seed, 'seed');
354
-
355
- // Derive unique seed for this key index
356
- const indexedSeed = deriveIndexedSeed(seed, i);
357
-
358
- // Expand indexed seed into private keys
359
- const privateKeys = expandPrivateKey(indexedSeed, ps);
360
-
361
- // Hash each chain 255 times to get chain tops
362
- const rounds = ps.maxDigit; // 255
363
- const buf = new Uint8Array(ps.L * 32);
364
-
365
- for (let j = 0; j < ps.L; j++) {
366
- const top = hashChain(privateKeys[j], rounds);
367
- buf.set(top, j * 32);
368
- }
369
-
370
- // Return H(all chain tops)
371
- return F(buf);
372
- }
373
-
374
- /**
375
- * Derive full WOTS public key (L × 32 bytes) without final hash
376
- *
377
- * Returns the concatenation of all chain tops BEFORE hashing.
378
- * Required by Minima's SignatureProof which expects the full public key.
379
- */
380
- export function deriveFullPublicKey(seed: Uint8Array, i: number, ps: ParamSet = getParamSet()): Uint8Array {
381
- assert32(seed, 'seed');
382
-
383
- if (wotsDebugEnabled) {
384
- wotsLogger.debug(`[deriveFullPublicKey] ========== PK DERIVATION START ==========`);
385
- wotsLogger.debug(`[deriveFullPublicKey] Master seed: ${hex(seed).substring(0, 32)}..., keyIndex: ${i}`);
386
- }
387
-
388
- // Derive unique seed for this key index
389
- const indexedSeed = deriveIndexedSeed(seed, i);
390
-
391
- if (wotsDebugEnabled) {
392
- wotsLogger.debug(`[deriveFullPublicKey] Indexed seed: ${hex(indexedSeed)}`);
393
- }
394
-
395
- const privateKeys = expandPrivateKey(indexedSeed, ps);
396
- const rounds = ps.maxDigit; // 255
397
- const buf = new Uint8Array(ps.L * 32);
398
-
399
- for (let j = 0; j < ps.L; j++) {
400
- const top = hashChain(privateKeys[j], rounds);
401
- buf.set(top, j * 32);
402
-
403
- // Log first few chain tops for debugging
404
- if (wotsDebugEnabled && j < 3) {
405
- wotsLogger.debug(`[deriveFullPublicKey] chain[${j}]: pk=${hex(privateKeys[j]).substring(0, 16)}... rounds=${rounds} => top=${hex(top).substring(0, 16)}...`);
406
- }
407
- }
408
-
409
- if (wotsDebugEnabled) {
410
- wotsLogger.debug(`[deriveFullPublicKey] Full public key (${buf.length}B): ${hex(buf).substring(0, 64)}...${hex(buf).substring(hex(buf).length - 32)}`);
411
- wotsLogger.debug(`[deriveFullPublicKey] ========== PK DERIVATION END ==========`);
412
- }
413
-
414
- return buf; // L * 32 = 1088 bytes
415
- }
416
-
417
- export function wotsKeypairFromSeed(seed: Uint8Array, index: number, paramSet?: ParamSet): WotsKeypair {
418
- assert32(seed, 'seed');
419
- const ps = paramSet || defaultParamSet;
420
- const pkDigest = derivePKdigest(seed, index, ps);
421
- return { seed, index, pk: pkDigest };
422
- }
423
-
424
- export type WotsSignature = {
425
- index: number;
426
- w: number;
427
- sig: Uint8Array[];
428
- };
429
-
430
- /**
431
- * Sign a message using WOTS
432
- *
433
- * Matches Java's WinternitzOTSignature.getSignature():
434
- * 1. Hash the message internally: hashedMsg = SHA3-256(message)
435
- * 2. For each digit d[i]: sig[i] = hash(privateKey[i], d[i] times)
436
- *
437
- * IMPORTANT: This function hashes the message internally to match Java/BouncyCastle.
438
- * Callers pass RAW 32-byte data (tx digest, child root), NOT pre-hashed!
439
- *
440
- * @param seed - 32-byte master seed
441
- * @param i - Key index
442
- * @param message - Raw 32-byte message (will be hashed internally)
443
- * @param ps - WOTS parameter set (default: minima)
444
- * @returns Flat signature (L × 32 = 1088 bytes)
445
- */
446
- export function wotsSign(seed: Uint8Array, i: number, message: Uint8Array, ps: ParamSet = getParamSet()): Uint8Array {
447
- assert32(seed, 'seed');
448
- assert32(message, 'message');
449
-
450
- if (wotsDebugEnabled) {
451
- wotsLogger.debug(`[wotsSign] ========== WOTS SIGN START ==========`);
452
- wotsLogger.debug(`[wotsSign] Master seed: ${hex(seed)}`);
453
- wotsLogger.debug(`[wotsSign] Key index: ${i}`);
454
- wotsLogger.debug(`[wotsSign] Message (raw): ${hex(message)}`);
455
- }
456
-
457
- // Derive unique seed for this key index
458
- const indexedSeed = deriveIndexedSeed(seed, i);
459
-
460
- if (wotsDebugEnabled) {
461
- wotsLogger.debug(`[wotsSign] Indexed seed (after deriveIndexedSeed): ${hex(indexedSeed)}`);
462
- }
463
-
464
- const privateKeys = expandPrivateKey(indexedSeed, ps);
465
- // CRITICAL: Hash the message internally to match Java/BouncyCastle Winternitz behavior
466
- // Java's WinternitzOTSignature.getSignature() always hashes the message first
467
- // Callers pass RAW 32-byte data (tx digest, child root), NOT pre-hashed!
468
- const hashedMsg = F(message);
469
- const { digits, checksumDigits } = toWinternitzDigits(hashedMsg, ps);
470
- const allDigits = digits.concat(checksumDigits);
471
-
472
- if (allDigits.length !== ps.L) {
473
- throw new Error(`Digit length mismatch: got ${allDigits.length}, want ${ps.L}`);
474
- }
475
-
476
- if (wotsDebugEnabled) {
477
- wotsLogger.debug(`[wotsSign] All ${allDigits.length} digits: [${allDigits.join(', ')}]`);
478
- }
479
-
480
- const out = new Uint8Array(ps.L * 32);
481
-
482
- for (let j = 0; j < ps.L; j++) {
483
- const rounds = allDigits[j];
484
- const sigPart = hashChain(privateKeys[j], rounds);
485
- out.set(sigPart, j * 32);
486
-
487
- // Log first few and last signature chains for debugging
488
- if (wotsDebugEnabled && (j < 3 || j >= ps.L - 2)) {
489
- wotsLogger.debug(`[wotsSign] chain[${j}]: pk=${hex(privateKeys[j]).substring(0, 16)}... rounds=${rounds} => sig=${hex(sigPart).substring(0, 16)}...`);
490
- }
491
- }
492
-
493
- if (wotsDebugEnabled) {
494
- wotsLogger.debug(`[wotsSign] Signature output (${out.length} bytes): ${hex(out).substring(0, 64)}...${hex(out).substring(hex(out).length - 64)}`);
495
- wotsLogger.debug(`[wotsSign] ========== WOTS SIGN END ==========`);
496
- }
497
-
498
- return out;
499
- }
500
-
501
- /**
502
- * Legacy wrapper returning structured signature
503
- */
504
- export function wotsSignLegacy(msgHash: Uint8Array, seed: Uint8Array, index: number, paramSet?: ParamSet): WotsSignature {
505
- const ps = paramSet || getParamSet();
506
- const sig = wotsSign(seed, index, msgHash, ps);
507
- const chunks: Uint8Array[] = [];
508
- for (let i = 0; i < ps.L; i++) {
509
- chunks.push(sig.subarray(i * 32, (i + 1) * 32));
510
- }
511
- return { index, w: ps.w, sig: chunks };
512
- }
513
-
514
- /**
515
- * Recover public key from signature
516
- *
517
- * Matches WinternitzOTSVerify.Verify():
518
- * 1. Hash the message: hashedMsg = SHA3-256(message)
519
- * 2. for each digit d[i]:
520
- * top[i] = hash(sig[i], (255 - d[i]) times)
521
- * 3. return H(concat(tops))
522
- *
523
- * CRITICAL: This function hashes the message internally to match Java/BouncyCastle
524
- * Winternitz behavior. Both wotsSign and wotsPkFromSig hash internally for parity.
525
- */
526
- export function wotsPkFromSig(message: Uint8Array, signature: WotsSignature, paramSet?: ParamSet): Uint8Array {
527
- const ps = paramSet || defaultParamSet;
528
- // CRITICAL: Hash the message internally to match Java/BouncyCastle Winternitz behavior
529
- const hashedMsg = F(message);
530
- const allDigits = baseWWithChecksum(hashedMsg, ps);
531
-
532
- if (signature.sig.length !== ps.L) {
533
- throw new Error(`Wrong signature length: expected ${ps.L}, got ${signature.sig.length}`);
534
- }
535
-
536
- const buf = new Uint8Array(ps.L * 32);
537
-
538
- for (let i = 0; i < ps.L; i++) {
539
- const stepsUp = ps.maxDigit - allDigits[i]; // 255 - d[i]
540
- const top = hashChain(signature.sig[i], stepsUp);
541
- buf.set(top, i * 32);
542
- }
543
-
544
- return F(buf);
545
- }
546
-
547
- /**
548
- * Verify WOTS signature against a FULL 1088-byte public key
549
- *
550
- * Matches Java's Winternitz.verify():
551
- * 1. Hash the message internally: hashedMsg = SHA3-256(message)
552
- * 2. Recover FULL public key from signature using hashedMsg (1088 bytes)
553
- * 3. Compare FULL reconstructed key to expected FULL public key
554
- *
555
- * CRITICAL: Java's Winternitz.verify() compares the FULL 1088-byte reconstructed
556
- * public key against the FULL 1088-byte stored public key, NOT a 32-byte digest!
557
- *
558
- * IMPORTANT: This function hashes the message internally to match Java/BouncyCastle.
559
- * Callers pass RAW 32-byte data (tx digest, child root), NOT pre-hashed!
560
- *
561
- * @param sig - The 1088-byte WOTS signature
562
- * @param message - Raw 32-byte message (will be hashed internally)
563
- * @param pkFull - The FULL 1088-byte WOTS public key (L=34 × 32 bytes)
564
- * @param ps - WOTS parameter set (default: minima)
565
- */
566
- export function wotsVerify(sig: Uint8Array, message: Uint8Array, pkFull: Uint8Array, ps: ParamSet = getParamSet()): boolean {
567
- // CRITICAL: Java verifies against FULL 1088-byte public key, not 32-byte digest
568
- const expectedLen = ps.L * 32;
569
- if (pkFull.length !== expectedLen) {
570
- throw new Error(`pkFull must be exactly ${expectedLen} bytes (L=${ps.L} × 32); got ${pkFull.length}`);
571
- }
572
-
573
- if (sig.length !== expectedLen) {
574
- return false;
575
- }
576
-
577
- // CRITICAL: Hash the message internally to match Java/BouncyCastle Winternitz behavior
578
- const hashedMsg = F(message);
579
- const allDigits = baseWWithChecksum(hashedMsg, ps);
580
- const buf = new Uint8Array(expectedLen);
581
-
582
- for (let j = 0; j < ps.L; j++) {
583
- const sigPart = sig.subarray(j * 32, (j + 1) * 32);
584
- const stepsUp = ps.maxDigit - allDigits[j];
585
- const top = hashChain(sigPart, stepsUp);
586
- buf.set(top, j * 32);
587
- }
588
-
589
- // Compare FULL 1088-byte reconstructed key to expected FULL public key
590
- // This matches Java: resp.isEqual(zPublicKey) where both are 1088 bytes
591
- if (buf.length !== pkFull.length) return false;
592
- let diff = 0;
593
- for (let i = 0; i < buf.length; i++) diff |= buf[i] ^ pkFull[i];
594
- return diff === 0;
595
- }
596
-
597
- /**
598
- * Legacy verify function that accepts a 32-byte digest
599
- * @deprecated Use wotsVerify with full 1088-byte public key instead
600
- */
601
- export function wotsVerifyDigest(sig: Uint8Array, message: Uint8Array, pkDigest: Uint8Array, ps: ParamSet = getParamSet()): boolean {
602
- assert32(pkDigest, 'pkDigest');
603
-
604
- const expectedLen = ps.L * 32;
605
- if (sig.length !== expectedLen) {
606
- return false;
607
- }
608
-
609
- const msgHash = F(message);
610
- const allDigits = baseWWithChecksum(msgHash, ps);
611
- const buf = new Uint8Array(expectedLen);
612
-
613
- for (let j = 0; j < ps.L; j++) {
614
- const sigPart = sig.subarray(j * 32, (j + 1) * 32);
615
- const stepsUp = ps.maxDigit - allDigits[j];
616
- const top = hashChain(sigPart, stepsUp);
617
- buf.set(top, j * 32);
618
- }
619
-
620
- const recomputed = F(buf);
621
- if (recomputed.length !== pkDigest.length) return false;
622
- let diff = 0;
623
- for (let i = 0; i < recomputed.length; i++) diff |= recomputed[i] ^ pkDigest[i];
624
- return diff === 0;
625
- }
626
-
627
-
628
- /**
629
- * Generate WOTS public key from seed (convenience wrapper)
630
- */
631
- export function wotsPublicKeyFromSeed(seed: Uint8Array, index: number = 0, paramSet?: ParamSet): Uint8Array {
632
- assert32(seed, 'seed');
633
- const keypair = wotsKeypairFromSeed(seed, index, paramSet);
634
- return keypair.pk;
635
- }
636
-
637
- // === Legacy compatibility exports ===
638
- // These are kept for backwards compatibility but use the BC-compatible implementation
639
-
640
- /**
641
- * @deprecated Use expandPrivateKey instead
642
- */
643
- export function prfChainSeed(seed: Uint8Array, i: number, j: number, _paramSet: ParamSet): Uint8Array {
644
- // This function no longer makes sense with stateful PRNG
645
- // For compatibility, derive the j-th chain seed for key i
646
- const privateKeys = expandPrivateKey(seed, _paramSet);
647
- if (j >= privateKeys.length) {
648
- throw new Error(`Chain index ${j} out of range (max ${privateKeys.length - 1})`);
649
- }
650
- return privateKeys[j];
651
- }