@totemsdk/core 1.0.0

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 (175) hide show
  1. package/dist/Streamable.d.ts +268 -0
  2. package/dist/Streamable.js +416 -0
  3. package/dist/adapters/index.d.ts +190 -0
  4. package/dist/adapters/index.js +107 -0
  5. package/dist/adapters/types.d.ts +149 -0
  6. package/dist/adapters/types.js +67 -0
  7. package/dist/binio.d.ts +9 -0
  8. package/dist/binio.js +30 -0
  9. package/dist/bip39.d.ts +91 -0
  10. package/dist/bip39.js +417 -0
  11. package/dist/derive.d.ts +2 -0
  12. package/dist/derive.js +9 -0
  13. package/dist/index.d.ts +30 -0
  14. package/dist/index.js +53 -0
  15. package/dist/javaStreamables.d.ts +233 -0
  16. package/dist/javaStreamables.js +332 -0
  17. package/dist/lease/LeaseMonitor.d.ts +44 -0
  18. package/dist/lease/LeaseMonitor.js +107 -0
  19. package/dist/lease/LeaseStore.d.ts +57 -0
  20. package/dist/lease/LeaseStore.js +144 -0
  21. package/dist/lease/WatermarkStore.d.ts +61 -0
  22. package/dist/lease/WatermarkStore.js +202 -0
  23. package/dist/lease/index.d.ts +14 -0
  24. package/dist/lease/index.js +13 -0
  25. package/dist/lease-client.d.ts +31 -0
  26. package/dist/lease-client.js +39 -0
  27. package/dist/minima32.d.ts +18 -0
  28. package/dist/minima32.js +112 -0
  29. package/dist/minimaWireSerializer.d.ts +60 -0
  30. package/dist/minimaWireSerializer.js +71 -0
  31. package/dist/mmr.d.ts +156 -0
  32. package/dist/mmr.js +342 -0
  33. package/dist/mx.d.ts +1 -0
  34. package/dist/mx.js +1 -0
  35. package/dist/params.d.ts +29 -0
  36. package/dist/params.js +16 -0
  37. package/dist/script.d.ts +7 -0
  38. package/dist/script.js +22 -0
  39. package/dist/scripts/contract-helpers.d.ts +252 -0
  40. package/dist/scripts/contract-helpers.js +394 -0
  41. package/dist/scripts/dapp-types.d.ts +119 -0
  42. package/dist/scripts/dapp-types.js +72 -0
  43. package/dist/scripts/index.d.ts +4 -0
  44. package/dist/scripts/index.js +4 -0
  45. package/dist/scripts/types.d.ts +81 -0
  46. package/dist/scripts/types.js +145 -0
  47. package/dist/scripts/witness-serializer.d.ts +29 -0
  48. package/dist/scripts/witness-serializer.js +288 -0
  49. package/dist/transaction.d.ts +106 -0
  50. package/dist/transaction.js +284 -0
  51. package/dist/treekey.d.ts +383 -0
  52. package/dist/treekey.js +762 -0
  53. package/dist/tx/TransactionLifecycle.d.ts +55 -0
  54. package/dist/tx/TransactionLifecycle.js +189 -0
  55. package/dist/tx/TransactionReceiptStore.d.ts +36 -0
  56. package/dist/tx/TransactionReceiptStore.js +85 -0
  57. package/dist/tx/TransactionService.d.ts +50 -0
  58. package/dist/tx/TransactionService.js +178 -0
  59. package/dist/tx/index.d.ts +13 -0
  60. package/dist/tx/index.js +12 -0
  61. package/dist/tx/types.d.ts +114 -0
  62. package/dist/tx/types.js +5 -0
  63. package/dist/util.d.ts +1 -0
  64. package/dist/util.js +1 -0
  65. package/dist/utils.d.ts +8 -0
  66. package/dist/utils.js +35 -0
  67. package/dist/verify.d.ts +18 -0
  68. package/dist/verify.js +162 -0
  69. package/dist/version.d.ts +12 -0
  70. package/dist/version.js +12 -0
  71. package/dist/wots.d.ts +232 -0
  72. package/dist/wots.js +538 -0
  73. package/package.json +65 -0
  74. package/src/Streamable.d.ts +265 -0
  75. package/src/Streamable.js +431 -0
  76. package/src/Streamable.ts +567 -0
  77. package/src/__tests__/Streamable.parity.test.ts +422 -0
  78. package/src/__tests__/perAddressDerivation.parity.test.ts +152 -0
  79. package/src/__tests__/wots-parity.test.ts +289 -0
  80. package/src/adapters/index.d.ts +190 -0
  81. package/src/adapters/index.js +117 -0
  82. package/src/adapters/index.ts +301 -0
  83. package/src/adapters/types.ts +235 -0
  84. package/src/addr.oracle.test.ts +25 -0
  85. package/src/address.test.ts +55 -0
  86. package/src/binio.ts +21 -0
  87. package/src/bip39.d.ts +91 -0
  88. package/src/bip39.js +427 -0
  89. package/src/bip39.ts +432 -0
  90. package/src/derive.d.ts +2 -0
  91. package/src/derive.js +13 -0
  92. package/src/derive.ts +11 -0
  93. package/src/index.d.ts +26 -0
  94. package/src/index.js +128 -0
  95. package/src/index.test.ts +5 -0
  96. package/src/index.ts +206 -0
  97. package/src/javaStreamables.d.ts +233 -0
  98. package/src/javaStreamables.js +328 -0
  99. package/src/javaStreamables.ts +403 -0
  100. package/src/lease/LeaseMonitor.d.ts +44 -0
  101. package/src/lease/LeaseMonitor.js +111 -0
  102. package/src/lease/LeaseMonitor.ts +157 -0
  103. package/src/lease/LeaseStore.d.ts +57 -0
  104. package/src/lease/LeaseStore.js +148 -0
  105. package/src/lease/LeaseStore.ts +203 -0
  106. package/src/lease/WatermarkStore.d.ts +61 -0
  107. package/src/lease/WatermarkStore.js +206 -0
  108. package/src/lease/WatermarkStore.ts +270 -0
  109. package/src/lease/index.d.ts +14 -0
  110. package/src/lease/index.js +22 -0
  111. package/src/lease/index.ts +41 -0
  112. package/src/lease-client.d.ts +31 -0
  113. package/src/lease-client.js +44 -0
  114. package/src/lease-client.ts +69 -0
  115. package/src/minima32.d.ts +10 -0
  116. package/src/minima32.js +113 -0
  117. package/src/minima32.test.ts +11 -0
  118. package/src/minima32.ts +127 -0
  119. package/src/minimaWireSerializer.d.ts +189 -0
  120. package/src/minimaWireSerializer.js +311 -0
  121. package/src/minimaWireSerializer.test.ts +466 -0
  122. package/src/minimaWireSerializer.ts +117 -0
  123. package/src/mmr.d.ts +153 -0
  124. package/src/mmr.js +351 -0
  125. package/src/mmr.oracle.test.ts +28 -0
  126. package/src/mmr.ts +429 -0
  127. package/src/mx.test.ts +25 -0
  128. package/src/mx.ts +1 -0
  129. package/src/params.d.ts +29 -0
  130. package/src/params.d.ts.map +1 -0
  131. package/src/params.js +20 -0
  132. package/src/params.js.map +1 -0
  133. package/src/params.ts +44 -0
  134. package/src/script.d.ts +7 -0
  135. package/src/script.js +27 -0
  136. package/src/script.ts +24 -0
  137. package/src/scripts/contract-helpers.ts +609 -0
  138. package/src/scripts/dapp-types.ts +240 -0
  139. package/src/scripts/index.ts +22 -0
  140. package/src/scripts/types.ts +247 -0
  141. package/src/scripts/witness-serializer.ts +384 -0
  142. package/src/transaction.ts +430 -0
  143. package/src/treekey.d.ts +383 -0
  144. package/src/treekey.js +778 -0
  145. package/src/treekey.parity.test.ts +459 -0
  146. package/src/treekey.ts +979 -0
  147. package/src/tx/TransactionLifecycle.d.ts +55 -0
  148. package/src/tx/TransactionLifecycle.js +188 -0
  149. package/src/tx/TransactionLifecycle.ts +280 -0
  150. package/src/tx/TransactionReceiptStore.d.ts +36 -0
  151. package/src/tx/TransactionReceiptStore.js +89 -0
  152. package/src/tx/TransactionReceiptStore.ts +112 -0
  153. package/src/tx/TransactionService.d.ts +31 -0
  154. package/src/tx/TransactionService.js +140 -0
  155. package/src/tx/TransactionService.ts +250 -0
  156. package/src/tx/index.d.ts +13 -0
  157. package/src/tx/index.js +20 -0
  158. package/src/tx/index.ts +44 -0
  159. package/src/tx/types.d.ts +80 -0
  160. package/src/tx/types.js +6 -0
  161. package/src/tx/types.ts +127 -0
  162. package/src/util.d.ts +1 -0
  163. package/src/util.js +5 -0
  164. package/src/util.ts +1 -0
  165. package/src/utils.d.ts +8 -0
  166. package/src/utils.js +42 -0
  167. package/src/utils.ts +38 -0
  168. package/src/verify.ts +209 -0
  169. package/src/version.ts +13 -0
  170. package/src/wots.d.ts +224 -0
  171. package/src/wots.d.ts.map +1 -0
  172. package/src/wots.js +559 -0
  173. package/src/wots.js.map +1 -0
  174. package/src/wots.ts +636 -0
  175. package/test-vectors.json +45 -0
@@ -0,0 +1,762 @@
1
+ /**
2
+ * TreeKey/TreeKeyNode Implementation matching Minima's TreeKey.java and TreeKeyNode.java
3
+ *
4
+ * This implements the hierarchical key tree structure where:
5
+ * - Each TreeKeyNode contains 64 Winternitz keys
6
+ * - The node's PUBLIC KEY = MMR root of all 64 Winternitz public keys
7
+ * - Signatures include the Winternitz leaf pubkey + signature + MMR proof
8
+ *
9
+ * Default structure: 3 levels x 64 keys = 64^3 = 262,144 one-time signatures
10
+ */
11
+ import { deriveChainSeedJava, hashObject, derivePerAddressSeed } from './javaStreamables.js';
12
+ import { derivePKdigest, deriveFullPublicKey, wotsSign, wotsVerifyDigest, hex } from './wots.js';
13
+ import { getParamSet } from './params.js';
14
+ import { MMRTree, createMMRDataLeafNode, calculateProofRoot, parseMMRProofFromHex } from './mmr.js';
15
+ import { writeSignature } from './Streamable.js';
16
+ import { NoopLogger } from './adapters/index.js';
17
+ // Module-level debug logger - can be set externally for parity testing
18
+ let treeKeyLogger = new NoopLogger();
19
+ let treeKeyDebugEnabled = false;
20
+ /**
21
+ * Enable TreeKey debug logging with a custom logger
22
+ * Use this for parity testing to capture wallet init intermediate values
23
+ *
24
+ * WARNING: Debug logging outputs sensitive cryptographic material including
25
+ * seeds and private key derivation. NEVER enable in production builds.
26
+ * This is intended for development/testing parity verification only.
27
+ */
28
+ export function setTreeKeyLogger(logger) {
29
+ treeKeyLogger = logger;
30
+ treeKeyDebugEnabled = true;
31
+ }
32
+ /**
33
+ * Disable TreeKey debug logging
34
+ */
35
+ export function disableTreeKeyLogger() {
36
+ treeKeyLogger = new NoopLogger();
37
+ treeKeyDebugEnabled = false;
38
+ }
39
+ /**
40
+ * Check if TreeKey debug logging is enabled
41
+ */
42
+ export function isTreeKeyDebugEnabled() {
43
+ return treeKeyDebugEnabled;
44
+ }
45
+ export const DEFAULT_KEYS_PER_LEVEL = 64;
46
+ export const DEFAULT_LEVELS = 3;
47
+ /**
48
+ * Compute root public key from a Winternitz signature proof
49
+ * Matches SignatureProof.getRootPublicKey() in Java
50
+ */
51
+ export function getRootPublicKey(proof) {
52
+ const leafData = createMMRDataLeafNode(proof.leafPubkey, 0n);
53
+ return calculateProofRoot(leafData, proof.mmrProof);
54
+ }
55
+ /**
56
+ * TreeKeyNode - One node in the key tree containing 64 Winternitz keys
57
+ *
58
+ * Matches TreeKeyNode.java (see attached_assets/TreeKeyNode_1767574401422.java):
59
+ *
60
+ * Key generation (lines 44-62):
61
+ * - Creates 64 Winternitz keys from a deterministic seed
62
+ * - For each key: MiniData pubkey = wots.getPublicKey() returns 32-byte DIGEST
63
+ * - Adds to MMR: MMRData.CreateMMRDataLeafNode(pubkey, MiniNumber.ZERO)
64
+ * - Public key = MMR root (mPublicKey = mTree.getRoot().getData())
65
+ *
66
+ * MMR leaf construction (see MMRData.java lines 30-36):
67
+ * MMRData.CreateMMRDataLeafNode(pubkeyDigest, MiniNumber.ZERO)
68
+ * → hash = Crypto.hashAllObjects(MiniNumber.ZERO, zData, zSumValue)
69
+ * → Serialization: [0x00,0x01,0x00] + [4-byte-len + pubkey] + [0x00,0x01,0x00]
70
+ *
71
+ * MMR parent construction (see MMRData.java lines 38-50):
72
+ * MMRData.CreateMMRDataParentNode(left, right)
73
+ * → hash = Crypto.hashAllObjects(MiniNumber.ONE, left.data, right.data, sumValue)
74
+ *
75
+ * IMPORTANT: Minima NEVER stores the 1088-byte full WOTS public key.
76
+ * Only the 32-byte digest is stored and used for MMR construction.
77
+ */
78
+ export class TreeKeyNode {
79
+ constructor(privateSeed, keysPerLevel = DEFAULT_KEYS_PER_LEVEL) {
80
+ this.childCache = new Map();
81
+ if (privateSeed.length !== 32) {
82
+ throw new Error('Private seed must be 32 bytes');
83
+ }
84
+ if (treeKeyDebugEnabled) {
85
+ treeKeyLogger.debug(`[TreeKeyNode] ========== INIT START ==========`);
86
+ treeKeyLogger.debug(`[TreeKeyNode] Private seed: ${hex(privateSeed)}`);
87
+ treeKeyLogger.debug(`[TreeKeyNode] Keys per level: ${keysPerLevel}`);
88
+ }
89
+ this.seed = privateSeed;
90
+ this.keysPerLevel = keysPerLevel;
91
+ // Hash the seed to create child seed (for deriving child nodes)
92
+ // From TreeKeyNode.java: mChildSeed = Crypto.getInstance().hashObject(zPrivateSeed);
93
+ // hashObject serializes as MiniData (length-prefixed) then hashes
94
+ this.childSeed = hashObject(privateSeed);
95
+ if (treeKeyDebugEnabled) {
96
+ treeKeyLogger.debug(`[TreeKeyNode] Child seed (hashObject): ${hex(this.childSeed)}`);
97
+ }
98
+ // Generate all Winternitz public key DIGESTS (32 bytes each)
99
+ // Java's Winternitz.getPublicKey() returns SHA3-256(full_key) = 32 bytes
100
+ // See WinternitzOTSignature.getPublicKey() lines 103-121: hashes full 1088B key, returns 32B digest
101
+ // TreeKeyNode.java line 53: MiniData pubkey = wots.getPublicKey() - stores the 32B digest
102
+ // MMRData.CreateMMRDataLeafNode(pubkey, MiniNumber.ZERO) - builds leaves from digests
103
+ this.publicKeyDigests = [];
104
+ for (let i = 0; i < keysPerLevel; i++) {
105
+ // Digest (32 bytes) - used for MMR tree, SignatureProof.leafPubkey, and address derivation
106
+ const pkDigest = derivePKdigest(privateSeed, i, getParamSet());
107
+ this.publicKeyDigests.push(pkDigest);
108
+ // Log first few keys for debugging
109
+ if (treeKeyDebugEnabled && i < 3) {
110
+ treeKeyLogger.debug(`[TreeKeyNode] WOTS key[${i}] digest (32B): ${hex(pkDigest)}`);
111
+ }
112
+ }
113
+ if (treeKeyDebugEnabled) {
114
+ treeKeyLogger.debug(`[TreeKeyNode] Generated ${this.publicKeyDigests.length} WOTS key digests`);
115
+ const lastIdx = this.publicKeyDigests.length - 1;
116
+ treeKeyLogger.debug(`[TreeKeyNode] WOTS key[${lastIdx}] digest: ${hex(this.publicKeyDigests[lastIdx])}`);
117
+ }
118
+ // Build MMR tree from 32-byte PUBLIC KEY DIGESTS (Java-compatible)
119
+ // Java's TreeKeyNode: MMRData.CreateMMRDataLeafNode(wots.getPublicKey(), ZERO)
120
+ // where getPublicKey() returns SHA3-256(full_key) = 32-byte DIGEST
121
+ this.mmrTree = MMRTree.fromPublicKeys(this.publicKeyDigests);
122
+ // Root public key = MMR root
123
+ const root = this.mmrTree.getRoot();
124
+ if (!root) {
125
+ throw new Error('Failed to compute MMR root');
126
+ }
127
+ this.rootPubkey = root.data;
128
+ if (treeKeyDebugEnabled) {
129
+ treeKeyLogger.debug(`[TreeKeyNode] MMR root (node public key): ${hex(this.rootPubkey)}`);
130
+ treeKeyLogger.debug(`[TreeKeyNode] ========== INIT END ==========`);
131
+ }
132
+ }
133
+ /**
134
+ * Async factory method for TreeKeyNode with progress reporting
135
+ * Yields to event loop every few keys to keep UI responsive
136
+ */
137
+ static async createWithProgress(privateSeed, keysPerLevel = DEFAULT_KEYS_PER_LEVEL, onProgress) {
138
+ if (privateSeed.length !== 32) {
139
+ throw new Error('Private seed must be 32 bytes');
140
+ }
141
+ const node = Object.create(TreeKeyNode.prototype);
142
+ node.seed = privateSeed;
143
+ node.keysPerLevel = keysPerLevel;
144
+ node.childSeed = hashObject(privateSeed);
145
+ node.publicKeyDigests = [];
146
+ node.childCache = new Map();
147
+ // Generate Winternitz public key DIGESTS with progress (32 bytes each)
148
+ // Java's Winternitz.getPublicKey() returns SHA3-256(full_key) = 32 bytes
149
+ // See WinternitzOTSignature.getPublicKey() lines 103-121: hashes full 1088B key, returns 32B digest
150
+ for (let i = 0; i < keysPerLevel; i++) {
151
+ // Digest (32 bytes) - used for MMR tree, SignatureProof.leafPubkey, and address derivation
152
+ const pkDigest = derivePKdigest(privateSeed, i, getParamSet());
153
+ node.publicKeyDigests.push(pkDigest);
154
+ // Report progress every 4 keys and yield to event loop
155
+ if (onProgress && (i + 1) % 4 === 0) {
156
+ onProgress({
157
+ phase: 'wots_keys',
158
+ current: i + 1,
159
+ total: keysPerLevel,
160
+ message: `Generating signing key ${i + 1}/${keysPerLevel}`
161
+ });
162
+ // Yield to event loop to keep UI responsive
163
+ await new Promise(resolve => setTimeout(resolve, 0));
164
+ }
165
+ }
166
+ // Report MMR build phase
167
+ if (onProgress) {
168
+ onProgress({
169
+ phase: 'mmr_build',
170
+ current: 0,
171
+ total: 1,
172
+ message: 'Building secure Merkle tree...'
173
+ });
174
+ }
175
+ // Build MMR tree from 32-byte PUBLIC KEY DIGESTS (Java-compatible)
176
+ // Java's TreeKeyNode: MMRData.CreateMMRDataLeafNode(wots.getPublicKey(), ZERO)
177
+ // where getPublicKey() returns SHA3-256(full_key) = 32-byte DIGEST
178
+ node.mmrTree = MMRTree.fromPublicKeys(node.publicKeyDigests);
179
+ const root = node.mmrTree.getRoot();
180
+ if (!root) {
181
+ throw new Error('Failed to compute MMR root');
182
+ }
183
+ node.rootPubkey = root.data;
184
+ if (onProgress) {
185
+ onProgress({
186
+ phase: 'mmr_build',
187
+ current: 1,
188
+ total: 1,
189
+ message: 'Merkle tree complete'
190
+ });
191
+ }
192
+ return node;
193
+ }
194
+ /**
195
+ * Get the public key for this tree node (MMR root of all 64 Winternitz keys)
196
+ */
197
+ getPublicKey() {
198
+ return this.rootPubkey;
199
+ }
200
+ /**
201
+ * Get the full Winternitz public key at a specific index (0-63)
202
+ * Returns the full L×32 byte public key (1088 bytes), derived on-demand
203
+ *
204
+ * NOTE: This is only used for local signature verification in tests.
205
+ * Java's Winternitz.getPublicKey() returns a 32-byte digest, not this.
206
+ * For production code, use getWOTSPublicKeyDigest() instead.
207
+ *
208
+ * @deprecated Use getWOTSPublicKeyDigest() for Minima compatibility
209
+ */
210
+ getWOTSPublicKey(index) {
211
+ if (index < 0 || index >= this.keysPerLevel) {
212
+ throw new Error(`Key index ${index} out of range [0, ${this.keysPerLevel})`);
213
+ }
214
+ // Derive on-demand - expensive but only used for tests
215
+ return deriveFullPublicKey(this.seed, index, getParamSet());
216
+ }
217
+ /**
218
+ * Get the Winternitz public key digest at a specific index (0-63)
219
+ * Returns the 32-byte SHA3 hash of the full public key
220
+ */
221
+ getWOTSPublicKeyDigest(index) {
222
+ if (index < 0 || index >= this.keysPerLevel) {
223
+ throw new Error(`Key index ${index} out of range [0, ${this.keysPerLevel})`);
224
+ }
225
+ return this.publicKeyDigests[index];
226
+ }
227
+ /**
228
+ * Get the MMR proof for a specific key index
229
+ */
230
+ getProof(keyIndex) {
231
+ return this.mmrTree.getProof(keyIndex);
232
+ }
233
+ /**
234
+ * Sign data with a specific key from this node
235
+ * Returns a SignatureProof containing the 32-byte leaf pubkey DIGEST, signature, and MMR proof
236
+ *
237
+ * CRITICAL: Java's WinternitzOTSignature.getSignature() ALWAYS hashes the message first,
238
+ * regardless of input length. From BouncyCastle WinternitzOTSignature.java lines 137-138:
239
+ * messDigestOTS.update(message, 0, message.length);
240
+ * messDigestOTS.doFinal(hash, 0);
241
+ *
242
+ * We MUST always hash to match Java verification, which also always hashes.
243
+ *
244
+ * CRITICAL FIX (January 2026): leafPubkey is the 32-byte WOTS public key DIGEST.
245
+ * Java's Winternitz.getPublicKey() returns SHA3-256(full_key) = 32 bytes!
246
+ * Previous bug: We stored 1088-byte full keys, Java expected 32-byte digests → verification failed.
247
+ */
248
+ sign(keyIndex, data) {
249
+ if (keyIndex < 0 || keyIndex >= this.keysPerLevel) {
250
+ throw new Error(`Key index ${keyIndex} out of range [0, ${this.keysPerLevel})`);
251
+ }
252
+ if (treeKeyDebugEnabled) {
253
+ treeKeyLogger.debug(`[TreeKeyNode.sign] ========== SIGN START ==========`);
254
+ treeKeyLogger.debug(`[TreeKeyNode.sign] keyIndex: ${keyIndex}`);
255
+ treeKeyLogger.debug(`[TreeKeyNode.sign] input data (${data.length}B): ${hex(data).substring(0, 64)}${data.length > 32 ? '...' : ''}`);
256
+ }
257
+ // Get the 32-byte Winternitz public key DIGEST for SignatureProof
258
+ // CRITICAL FIX: Java's Winternitz.getPublicKey() returns SHA3-256(full_key) = 32 bytes!
259
+ // NOT the 1088-byte full key! This was the bug causing "allsignaturesvalid":false errors.
260
+ const leafPubkey = this.publicKeyDigests[keyIndex];
261
+ if (treeKeyDebugEnabled) {
262
+ treeKeyLogger.debug(`[TreeKeyNode.sign] leafPubkey (${leafPubkey.length}B DIGEST): ${hex(leafPubkey)}`);
263
+ }
264
+ // CRITICAL: Do NOT pre-hash here! wotsSign() now handles internal hashing (matching Java/BouncyCastle).
265
+ // Java's WinternitzOTSignature.getSignature() hashes internally, so wotsSign() does too.
266
+ // Call sites pass the same 32-byte values Java passes:
267
+ // - For TX signing: the 32-byte transaction digest
268
+ // - For parent→child: the 32-byte childRoot (MMR root of child proof)
269
+ // WOTS hashes once internally - this is by design for Minima compatibility.
270
+ if (treeKeyDebugEnabled) {
271
+ treeKeyLogger.debug(`[TreeKeyNode.sign] dataToSign (raw input, WOTS hashes internally): ${hex(data)}`);
272
+ }
273
+ // Sign data directly - wotsSign() handles internal hashing
274
+ const signature = wotsSign(this.seed, keyIndex, data, getParamSet());
275
+ if (treeKeyDebugEnabled) {
276
+ treeKeyLogger.debug(`[TreeKeyNode.sign] signature (${signature.length}B): ${hex(signature).substring(0, 64)}...${hex(signature).substring(hex(signature).length - 32)}`);
277
+ }
278
+ // Get the MMR proof for this key
279
+ const mmrProof = this.getProof(keyIndex);
280
+ if (treeKeyDebugEnabled) {
281
+ treeKeyLogger.debug(`[TreeKeyNode.sign] MMR proof chunks: ${mmrProof.chunks.length}`);
282
+ treeKeyLogger.debug(`[TreeKeyNode.sign] ========== SIGN END ==========`);
283
+ }
284
+ return { leafPubkey, signature, mmrProof };
285
+ }
286
+ /**
287
+ * Create a child TreeKeyNode at the specified index
288
+ * Matches TreeKeyNode.java getChild()
289
+ *
290
+ * PERFORMANCE FIX: Child nodes are now cached to avoid regenerating
291
+ * 64 WOTS keys on every getChild() call. This is critical for address
292
+ * derivation performance where getChild() is called 64 times.
293
+ */
294
+ getChild(childIndex) {
295
+ if (childIndex < 0 || childIndex >= this.keysPerLevel) {
296
+ throw new Error(`Child index ${childIndex} out of range [0, ${this.keysPerLevel})`);
297
+ }
298
+ // Check cache first
299
+ const cached = this.childCache.get(childIndex);
300
+ if (cached) {
301
+ if (treeKeyDebugEnabled) {
302
+ treeKeyLogger.debug(`[TreeKeyNode.getChild] Using cached child at index ${childIndex}`);
303
+ }
304
+ return cached;
305
+ }
306
+ if (treeKeyDebugEnabled) {
307
+ treeKeyLogger.debug(`[TreeKeyNode.getChild] Creating child at index ${childIndex}`);
308
+ treeKeyLogger.debug(`[TreeKeyNode.getChild] Parent childSeed: ${hex(this.childSeed)}`);
309
+ }
310
+ // From TreeKeyNode.java:
311
+ // MiniData seed = Crypto.getInstance().hashAllObjects(new MiniNumber(zChild), mChildSeed);
312
+ const childSeed = deriveChainSeedJava(this.childSeed, childIndex);
313
+ if (treeKeyDebugEnabled) {
314
+ treeKeyLogger.debug(`[TreeKeyNode.getChild] Derived childSeed: ${hex(childSeed)}`);
315
+ }
316
+ const child = new TreeKeyNode(childSeed, this.keysPerLevel);
317
+ if (treeKeyDebugEnabled) {
318
+ treeKeyLogger.debug(`[TreeKeyNode.getChild] Child public key: ${hex(child.getPublicKey())}`);
319
+ }
320
+ // Cache for future use
321
+ this.childCache.set(childIndex, child);
322
+ return child;
323
+ }
324
+ }
325
+ /**
326
+ * TreeKey - Full hierarchical key tree with multiple levels
327
+ *
328
+ * Matches TreeKey.java:
329
+ * - Default: 3 levels x 64 keys = 262,144 one-time signatures
330
+ * - Tracks usage count to determine which key to use
331
+ * - Produces multi-level signatures
332
+ */
333
+ export class TreeKey {
334
+ constructor(privateSeed, keysPerLevel = DEFAULT_KEYS_PER_LEVEL, levels = DEFAULT_LEVELS) {
335
+ this.uses = 0;
336
+ /**
337
+ * Parent-child signature cache for efficiency
338
+ * Key format: "l1" for root->level1, "l1,l2" for level1->level2
339
+ * Value: SignatureProof for parent signing child's pubkey
340
+ */
341
+ this.parentChildSigCache = new Map();
342
+ if (privateSeed.length !== 32) {
343
+ throw new Error('Private seed must be 32 bytes');
344
+ }
345
+ if (treeKeyDebugEnabled) {
346
+ treeKeyLogger.debug(`[TreeKey] ========== WALLET INIT START ==========`);
347
+ treeKeyLogger.debug(`[TreeKey] Private seed: ${hex(privateSeed)}`);
348
+ treeKeyLogger.debug(`[TreeKey] Levels: ${levels}, Keys per level: ${keysPerLevel}`);
349
+ treeKeyLogger.debug(`[TreeKey] Total capacity: ${Math.pow(keysPerLevel, levels)} signatures`);
350
+ }
351
+ this.privateSeed = privateSeed;
352
+ this.levels = levels;
353
+ this.keysPerLevel = keysPerLevel;
354
+ // Initialize root node
355
+ this.root = new TreeKeyNode(privateSeed, keysPerLevel);
356
+ this.publicKey = this.root.getPublicKey();
357
+ if (treeKeyDebugEnabled) {
358
+ treeKeyLogger.debug(`[TreeKey] Root public key: ${hex(this.publicKey)}`);
359
+ treeKeyLogger.debug(`[TreeKey] ========== WALLET INIT END ==========`);
360
+ }
361
+ }
362
+ /**
363
+ * Async factory method for TreeKey with progress reporting
364
+ * Reports progress as the root TreeKeyNode generates its 64 signing keys
365
+ */
366
+ static async createWithProgress(privateSeed, keysPerLevel = DEFAULT_KEYS_PER_LEVEL, levels = DEFAULT_LEVELS, onProgress) {
367
+ if (privateSeed.length !== 32) {
368
+ throw new Error('Private seed must be 32 bytes');
369
+ }
370
+ const treeKey = Object.create(TreeKey.prototype);
371
+ treeKey.privateSeed = privateSeed;
372
+ treeKey.levels = levels;
373
+ treeKey.keysPerLevel = keysPerLevel;
374
+ treeKey.uses = 0;
375
+ treeKey.parentChildSigCache = new Map();
376
+ // Create root node with progress reporting
377
+ treeKey.root = await TreeKeyNode.createWithProgress(privateSeed, keysPerLevel, onProgress);
378
+ treeKey.publicKey = treeKey.root.getPublicKey();
379
+ if (onProgress) {
380
+ onProgress({
381
+ phase: 'complete',
382
+ current: keysPerLevel,
383
+ total: keysPerLevel,
384
+ message: 'Key tree ready'
385
+ });
386
+ }
387
+ return treeKey;
388
+ }
389
+ /**
390
+ * Get the root TreeKeyNode (for internal use)
391
+ */
392
+ getRootNode() {
393
+ return this.root;
394
+ }
395
+ /**
396
+ * Get the root public key (for watermark tracking)
397
+ */
398
+ getRootPublicKey() {
399
+ return this.publicKey;
400
+ }
401
+ /**
402
+ * Get the wallet's public key (root of the key tree)
403
+ */
404
+ getPublicKey() {
405
+ return this.publicKey;
406
+ }
407
+ /**
408
+ * Get the maximum number of signatures this tree can produce
409
+ */
410
+ getMaxUses() {
411
+ return Math.pow(this.keysPerLevel, this.levels);
412
+ }
413
+ /**
414
+ * Get current usage count
415
+ */
416
+ getUses() {
417
+ return this.uses;
418
+ }
419
+ /**
420
+ * Set the usage counter (for resuming from a known state)
421
+ */
422
+ setUses(uses) {
423
+ this.uses = uses;
424
+ }
425
+ /**
426
+ * Generate cache key for parent-child signature
427
+ * @param path - Array of indices leading to the child (e.g., [l1] or [l1, l2])
428
+ */
429
+ getCacheKey(path) {
430
+ return path.join(',');
431
+ }
432
+ /**
433
+ * Check if a parent-child signature is cached
434
+ * @param path - Array of indices (e.g., [l1] for root->level1)
435
+ */
436
+ hasParentChildSig(path) {
437
+ return this.parentChildSigCache.has(this.getCacheKey(path));
438
+ }
439
+ /**
440
+ * Get a cached parent-child signature
441
+ * @param path - Array of indices (e.g., [l1] for root->level1)
442
+ */
443
+ getParentChildSig(path) {
444
+ return this.parentChildSigCache.get(this.getCacheKey(path));
445
+ }
446
+ /**
447
+ * Cache a parent-child signature for reuse
448
+ * This allows the same signature to be reused across multiple signing operations
449
+ *
450
+ * @param path - Array of indices leading to the child (e.g., [l1] or [l1, l2])
451
+ * @param sig - SignatureProof from parent signing child's public key
452
+ */
453
+ setParentChildSig(path, sig) {
454
+ this.parentChildSigCache.set(this.getCacheKey(path), sig);
455
+ }
456
+ /**
457
+ * Get all cached parent-child signatures (for serialization/persistence)
458
+ */
459
+ getCachedSignatures() {
460
+ return new Map(this.parentChildSigCache);
461
+ }
462
+ /**
463
+ * Restore cached signatures (for hydrating from persistence)
464
+ */
465
+ restoreCachedSignatures(cache) {
466
+ this.parentChildSigCache = new Map(cache);
467
+ }
468
+ /**
469
+ * Convert a usage number to the path through the tree
470
+ * Matches TreeKey.java baseConversion()
471
+ *
472
+ * For uses=0: [0,0,0]
473
+ * For uses=1: [0,0,1]
474
+ * For uses=64: [0,1,0]
475
+ * etc.
476
+ */
477
+ baseConversion(num) {
478
+ const result = [];
479
+ let counter = num;
480
+ while (counter !== 0) {
481
+ const div = Math.floor(counter / this.keysPerLevel);
482
+ const remain = counter - (div * this.keysPerLevel);
483
+ result.push(remain);
484
+ counter = div;
485
+ }
486
+ // Pad to required levels
487
+ while (result.length < this.levels) {
488
+ result.push(0);
489
+ }
490
+ // Reverse to get [addressIndex, l1, l2] order
491
+ result.reverse();
492
+ return result;
493
+ }
494
+ /**
495
+ * Sign data with the current key and increment usage
496
+ *
497
+ * Matches TreeKey.java sign():
498
+ * - Determines path through tree based on usage count
499
+ * - Each level's key signs the next level's root public key
500
+ * - Final level signs the actual data
501
+ *
502
+ * CRITICAL FIX (January 2026): Build proofs bottom-up to sign child's getRootPublicKey()
503
+ *
504
+ * Java's TreeKey.verify() verifies non-leaf signatures against childsig.getRootPublicKey(),
505
+ * which is the 32-byte MMR root computed from the NEXT proof's leafPubkey + MMRproof.
506
+ *
507
+ * Uses parent-child signature caching for efficiency:
508
+ * - Parent-child signatures are cached and reused
509
+ * - Only the final data signature is computed fresh each time
510
+ */
511
+ sign(data) {
512
+ if (this.uses >= this.getMaxUses()) {
513
+ throw new Error('No more keys available (tree exhausted)');
514
+ }
515
+ // Get the path through the tree
516
+ const path = this.baseConversion(this.uses);
517
+ // Navigate to all nodes first
518
+ const nodes = [this.root];
519
+ let currentNode = this.root;
520
+ for (let i = 0; i < this.levels - 1; i++) {
521
+ currentNode = currentNode.getChild(path[i]);
522
+ nodes.push(currentNode);
523
+ }
524
+ // nodes = [root, L1, L2] for a 3-level tree
525
+ // ═══════════════════════════════════════════════════════════════════════════
526
+ // BUILD PROOFS BOTTOM-UP: Start from leaf, work up to root
527
+ // This ensures we sign exactly what Java's verification will compute.
528
+ // ═══════════════════════════════════════════════════════════════════════════
529
+ const proofs = new Array(this.levels);
530
+ // STEP 1: Create leaf proof FIRST (signs actual data)
531
+ const leafDepth = this.levels - 1;
532
+ const leafNode = nodes[leafDepth];
533
+ const leafKeyIndex = path[leafDepth];
534
+ const leafProof = leafNode.sign(leafKeyIndex, data);
535
+ proofs[leafDepth] = leafProof;
536
+ // STEP 2: Work backwards, each parent signs childProof.getRootPublicKey()
537
+ for (let depth = this.levels - 2; depth >= 0; depth--) {
538
+ const parentNode = nodes[depth];
539
+ const keyIndex = path[depth];
540
+ const childProof = proofs[depth + 1];
541
+ const childRoot = getRootPublicKey(childProof);
542
+ // Cache key includes full path to ensure uniqueness
543
+ const cachePath = path.slice(0, depth + 2);
544
+ let sigProof = this.getParentChildSig(cachePath);
545
+ if (!sigProof) {
546
+ // Not cached: compute and cache
547
+ sigProof = parentNode.sign(keyIndex, childRoot); // Sign the 32-byte MMR root!
548
+ this.setParentChildSig(cachePath, sigProof);
549
+ }
550
+ proofs[depth] = sigProof;
551
+ }
552
+ // Increment usage counter
553
+ this.uses++;
554
+ return { proofs };
555
+ }
556
+ /**
557
+ * Get the public key for a level-1 address (single index)
558
+ * This is the MMR root of the level-1 TreeKeyNode's 64 Winternitz keys.
559
+ *
560
+ * Use this for wallet addresses where each address = one level-1 node.
561
+ *
562
+ * @param l1 - Level 1 index (0-63, corresponds to wallet address index)
563
+ * @returns 32-byte MMR root public key for SIGNEDBY scripts
564
+ */
565
+ getAddressPublicKey(l1) {
566
+ // Get level-1 child node's public key (MMR root of its 64 WOTS keys)
567
+ const level1Node = this.root.getChild(l1);
568
+ return level1Node.getPublicKey();
569
+ }
570
+ /**
571
+ * Get the public key for a specific signing key at tree index (l1, l2)
572
+ * This navigates to the level-2 node for signing operations.
573
+ *
574
+ * @param l1 - Level 1 index (address)
575
+ * @param l2 - Level 2 index (signing key within address)
576
+ * @returns 32-byte MMR root public key of level-2 node
577
+ */
578
+ getSigningNodePublicKey(l1, l2) {
579
+ // Navigate to level 2 node
580
+ const level1Node = this.root.getChild(l1);
581
+ const level2Node = level1Node.getChild(l2);
582
+ return level2Node.getPublicKey();
583
+ }
584
+ }
585
+ /**
586
+ * Verify a tree signature against expected public key and data
587
+ *
588
+ * Matches TreeKey.java verify():
589
+ * - First proof's computed root must match expected public key
590
+ * - Each intermediate proof must sign the next level's root
591
+ * - Final proof must verify against the actual data
592
+ */
593
+ export function verifyTreeSignature(expectedPubkey, data, signature) {
594
+ const { proofs } = signature;
595
+ if (proofs.length === 0) {
596
+ return false;
597
+ }
598
+ // Import wotsVerify for verification
599
+ // wotsVerify already imported at top of file
600
+ const paramSet = getParamSet();
601
+ for (let depth = 0; depth < proofs.length; depth++) {
602
+ const proof = proofs[depth];
603
+ // Compute the root public key from this proof
604
+ const rootPubkey = getRootPublicKey(proof);
605
+ // First level: must match expected public key
606
+ if (depth === 0) {
607
+ for (let i = 0; i < expectedPubkey.length; i++) {
608
+ if (rootPubkey[i] !== expectedPubkey[i]) {
609
+ return false;
610
+ }
611
+ }
612
+ }
613
+ // Determine what was signed
614
+ let signedData;
615
+ if (depth === proofs.length - 1) {
616
+ // Final level: signed the actual data
617
+ signedData = data;
618
+ }
619
+ else {
620
+ // Intermediate level: signed the next level's root public key
621
+ signedData = getRootPublicKey(proofs[depth + 1]);
622
+ }
623
+ // Verify the Winternitz signature
624
+ // Use wotsVerifyDigest since leafPubkey is now a 32-byte digest (not 1088-byte full key)
625
+ if (!wotsVerifyDigest(proof.signature, signedData, proof.leafPubkey, paramSet)) {
626
+ return false;
627
+ }
628
+ }
629
+ return true;
630
+ }
631
+ /**
632
+ * Serialize a TreeSignature to bytes
633
+ *
634
+ * Uses Streamable.writeSignature() for byte-exact compatibility
635
+ * with Java's Signature.writeDataStream().
636
+ */
637
+ export function serializeTreeSignature(sig) {
638
+ // Convert to Streamable format (adds blockTime to MMRProof)
639
+ const streamableSig = {
640
+ proofs: sig.proofs.map(proof => ({
641
+ leafPubkey: proof.leafPubkey,
642
+ signature: proof.signature,
643
+ mmrProof: {
644
+ blockTime: 0n, // Default for compatibility
645
+ chunks: proof.mmrProof.chunks
646
+ }
647
+ }))
648
+ };
649
+ // Use canonical serializer for byte-exact Java compatibility
650
+ return writeSignature(streamableSig);
651
+ }
652
+ /**
653
+ * Deserialize a TreeSignature from bytes
654
+ *
655
+ * Matches Java's Signature.readDataStream():
656
+ * - Number of proofs: MiniNumber format
657
+ * - Each SignatureProof: MiniData(pubkey) + MiniData(signature) + MMRProof
658
+ */
659
+ export function deserializeTreeSignature(data) {
660
+ let offset = 0;
661
+ // Read MiniNumber for proof count
662
+ // Format: [scale: 1 byte] [length: 1 byte] [data: N bytes]
663
+ const scale = data[offset];
664
+ const numBytesLen = data[offset + 1];
665
+ offset += 2;
666
+ // Read the number bytes (big-endian)
667
+ let numProofs = 0;
668
+ for (let i = 0; i < numBytesLen; i++) {
669
+ numProofs = (numProofs << 8) | data[offset + i];
670
+ }
671
+ offset += numBytesLen;
672
+ const proofs = [];
673
+ for (let i = 0; i < numProofs; i++) {
674
+ // Leaf pubkey - MiniData format (4-byte length prefix + data)
675
+ const pubkeyLen = (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3];
676
+ offset += 4;
677
+ const leafPubkey = data.slice(offset, offset + pubkeyLen);
678
+ offset += pubkeyLen;
679
+ // Signature - MiniData format (4-byte length prefix + data)
680
+ const sigLen = (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3];
681
+ offset += 4;
682
+ const signature = data.slice(offset, offset + sigLen);
683
+ offset += sigLen;
684
+ // MMR proof - new format returns { proof, blockTime }
685
+ const { proof: mmrProof } = parseMMRProofFromHex(data.slice(offset));
686
+ // Calculate how many bytes the MMR proof consumed
687
+ // Format: blockTime (MiniNumber: 3 bytes for 0) + length (MiniNumber: 3 bytes for count)
688
+ // + each chunk (1 byte isLeft + 32 bytes hash + 3 bytes value MiniNumber)
689
+ const mmrProofLen = 3 + 3 + (mmrProof.chunks.length * (1 + 32 + 3));
690
+ offset += mmrProofLen;
691
+ proofs.push({ leafPubkey, signature, mmrProof });
692
+ }
693
+ return { proofs };
694
+ }
695
+ /**
696
+ * ============================================================================
697
+ * PER-ADDRESS TREEKEY FACTORY
698
+ * ============================================================================
699
+ *
700
+ * Minima Wallet.createNewKey() creates independent TreeKeys per address:
701
+ * MiniData modifier = new MiniData(new BigInteger(Integer.toString(numkeys)));
702
+ * MiniData privseed = Crypto.getInstance().hashObjects(baseSeed, modifier);
703
+ * TreeKey treekey = TreeKey.createDefault(privseed);
704
+ *
705
+ * Each address gets its own TreeKey with:
706
+ * - Unique seed derived from baseSeed + addressIndex
707
+ * - Size=64, depth=3 (matching TreeKey.createDefault)
708
+ * - 4096 one-time signatures per address (64×64)
709
+ *
710
+ * Total wallet capacity: 64 addresses × 4096 signatures = 262,144 signatures
711
+ * ============================================================================
712
+ */
713
+ /**
714
+ * Create a per-address TreeKey matching Minima Wallet.createNewKey()
715
+ *
716
+ * @param baseSeed - 32-byte wallet base seed (from mnemonic)
717
+ * @param addressIndex - Address index (0-63)
718
+ * @returns TreeKey for this address with size=64, depth=3
719
+ */
720
+ export function createPerAddressTreeKey(baseSeed, addressIndex) {
721
+ const addressSeed = derivePerAddressSeed(baseSeed, addressIndex);
722
+ return new TreeKey(addressSeed, 64, 3);
723
+ }
724
+ /**
725
+ * Async version with progress reporting for UI
726
+ *
727
+ * @param baseSeed - 32-byte wallet base seed
728
+ * @param addressIndex - Address index (0-63)
729
+ * @param onProgress - Optional progress callback
730
+ * @returns Promise resolving to TreeKey for this address
731
+ */
732
+ export async function createPerAddressTreeKeyAsync(baseSeed, addressIndex, onProgress) {
733
+ const addressSeed = derivePerAddressSeed(baseSeed, addressIndex);
734
+ return TreeKey.createWithProgress(addressSeed, 64, 3, onProgress);
735
+ }
736
+ /**
737
+ * Derive address public key from base seed and address index
738
+ *
739
+ * This is the fast path for getting an address public key without
740
+ * needing to construct the full TreeKey. Useful for address derivation
741
+ * during wallet initialization.
742
+ *
743
+ * @param baseSeed - 32-byte wallet base seed
744
+ * @param addressIndex - Address index (0-63)
745
+ * @returns 32-byte address public key (MMR root of per-address TreeKey)
746
+ */
747
+ export function deriveAddressPublicKey(baseSeed, addressIndex) {
748
+ const treeKey = createPerAddressTreeKey(baseSeed, addressIndex);
749
+ return treeKey.getPublicKey();
750
+ }
751
+ /**
752
+ * Get the address public key from a per-address TreeKey
753
+ *
754
+ * In the per-address architecture, each address has its own TreeKey,
755
+ * and the address public key is simply the TreeKey's root public key.
756
+ *
757
+ * @param treeKey - Per-address TreeKey
758
+ * @returns 32-byte address public key (TreeKey root)
759
+ */
760
+ export function getPerAddressPublicKey(treeKey) {
761
+ return treeKey.getPublicKey();
762
+ }