@orbinum/sdk 0.25.0 → 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.
- package/README.md +355 -23
- package/dist/adapters/indexeddb/index.d.mts +152 -0
- package/dist/adapters/indexeddb/index.d.ts +152 -0
- package/dist/adapters/indexeddb/index.js +381 -0
- package/dist/adapters/indexeddb/index.mjs +326 -0
- package/dist/chunk-JMYU5QAK.mjs +40 -0
- package/dist/chunk-Y6LNYJAJ.mjs +1242 -0
- package/dist/index-JYVjYJtf.d.mts +353 -0
- package/dist/index-JYVjYJtf.d.ts +353 -0
- package/dist/index.d.mts +4745 -2971
- package/dist/index.d.ts +4745 -2971
- package/dist/index.js +6319 -3431
- package/dist/index.mjs +4884 -3259
- package/dist/secretStore-CF6Nse__.d.mts +292 -0
- package/dist/secretStore-CF6Nse__.d.ts +292 -0
- package/dist/wallet/worker/index.d.mts +1 -0
- package/dist/wallet/worker/index.d.ts +1 -0
- package/dist/wallet/worker/index.js +859 -0
- package/dist/wallet/worker/index.mjs +28 -0
- package/package.json +24 -7
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/wallet/worker/index.ts
|
|
21
|
+
var worker_exports = {};
|
|
22
|
+
__export(worker_exports, {
|
|
23
|
+
DECRYPT_YIELD_EVERY: () => DECRYPT_YIELD_EVERY,
|
|
24
|
+
EMPTY_BATCH_RESULT: () => EMPTY_BATCH_RESULT,
|
|
25
|
+
MAX_WORKERS: () => MAX_WORKERS,
|
|
26
|
+
PAIRWISE_EPH_WINDOW: () => PAIRWISE_EPH_WINDOW,
|
|
27
|
+
SELF_EPH_WINDOW: () => SELF_EPH_WINDOW,
|
|
28
|
+
WORKER_CRASHED: () => WORKER_CRASHED,
|
|
29
|
+
clearKnownEphWindow: () => clearKnownEphWindow,
|
|
30
|
+
createDecryptPool: () => createDecryptPool,
|
|
31
|
+
createMainThreadPool: () => createMainThreadPool,
|
|
32
|
+
createWorkerPool: () => createWorkerPool,
|
|
33
|
+
decryptHintBatch: () => decryptHintBatch,
|
|
34
|
+
getKnownEphWindow: () => getKnownEphWindow
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(worker_exports);
|
|
37
|
+
|
|
38
|
+
// src/protocol/memo/EncryptedMemo.ts
|
|
39
|
+
var import_chacha = require("@noble/ciphers/chacha.js");
|
|
40
|
+
var import_utils = require("@noble/ciphers/utils.js");
|
|
41
|
+
var import_baby_jubjub = require("@zk-kit/baby-jubjub");
|
|
42
|
+
|
|
43
|
+
// src/foundation/crypto/bjj-fast.ts
|
|
44
|
+
var import_edwards = require("@noble/curves/abstract/edwards.js");
|
|
45
|
+
var P = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
46
|
+
var N = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
|
|
47
|
+
var BjjPoint = (0, import_edwards.edwards)({
|
|
48
|
+
p: P,
|
|
49
|
+
n: N,
|
|
50
|
+
h: 8n,
|
|
51
|
+
a: 168700n,
|
|
52
|
+
d: 168696n,
|
|
53
|
+
Gx: 5299619240641551281634865583518297030282874472190772894086521144482721001553n,
|
|
54
|
+
Gy: 16950150798460657717958625567821834550301663161624707787222815936182638968203n
|
|
55
|
+
});
|
|
56
|
+
function fastMulBase(scalar) {
|
|
57
|
+
const s = scalar % N;
|
|
58
|
+
if (s === 0n) return [0n, 1n];
|
|
59
|
+
const { x, y } = BjjPoint.BASE.multiply(s).toAffine();
|
|
60
|
+
return [x, y];
|
|
61
|
+
}
|
|
62
|
+
function fastMulPoint(point, scalar) {
|
|
63
|
+
const s = scalar % N;
|
|
64
|
+
if (s === 0n) return [0n, 1n];
|
|
65
|
+
const { x, y } = BjjPoint.fromAffine({ x: point[0], y: point[1] }).multiplyUnsafe(s).toAffine();
|
|
66
|
+
return [x, y];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/foundation/encoding/hex.ts
|
|
70
|
+
function toHex(bytes) {
|
|
71
|
+
return "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
72
|
+
}
|
|
73
|
+
function fromHex(hex) {
|
|
74
|
+
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
75
|
+
if (clean.length % 2 !== 0) {
|
|
76
|
+
throw new Error(`Invalid hex string \u2014 odd length: "${hex}"`);
|
|
77
|
+
}
|
|
78
|
+
const bytes = new Uint8Array(clean.length / 2);
|
|
79
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
80
|
+
const byte = parseInt(clean.slice(i * 2, i * 2 + 2), 16);
|
|
81
|
+
if (isNaN(byte)) throw new Error(`Invalid hex character at position ${i * 2}`);
|
|
82
|
+
bytes[i] = byte;
|
|
83
|
+
}
|
|
84
|
+
return bytes;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/foundation/encoding/bytes.ts
|
|
88
|
+
function bigintTo32Le(n) {
|
|
89
|
+
const buf = new Uint8Array(32);
|
|
90
|
+
let v = n;
|
|
91
|
+
for (let i = 0; i < 32; i++) {
|
|
92
|
+
buf[i] = Number(v & 0xffn);
|
|
93
|
+
v >>= 8n;
|
|
94
|
+
}
|
|
95
|
+
return buf;
|
|
96
|
+
}
|
|
97
|
+
function bytesToBigintLE(bytes) {
|
|
98
|
+
let result = 0n;
|
|
99
|
+
for (let i = bytes.length - 1; i >= 0; i--) {
|
|
100
|
+
result = result << 8n | BigInt(bytes[i] ?? 0);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/foundation/crypto/constants.ts
|
|
106
|
+
var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
107
|
+
var BABYJUB_SUBORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
|
|
108
|
+
|
|
109
|
+
// src/protocol/memo/plaintext.ts
|
|
110
|
+
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
111
|
+
var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
|
|
112
|
+
var VIEW_TAG_DOMAIN = new TextEncoder().encode("orbinum-view-tag-v1");
|
|
113
|
+
var MEMO_PLAINTEXT_SIZE = 120;
|
|
114
|
+
function serializeMemo(value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion) {
|
|
115
|
+
const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
|
|
116
|
+
const view = new DataView(buf.buffer);
|
|
117
|
+
view.setBigUint64(0, value & 0xffffffffffffffffn, true);
|
|
118
|
+
view.setBigUint64(8, value >> 64n & 0xffffffffffffffffn, true);
|
|
119
|
+
buf.set(ownerPk.slice(0, 32), 16);
|
|
120
|
+
buf.set(blinding.slice(0, 32), 48);
|
|
121
|
+
view.setUint32(80, assetId >>> 0, true);
|
|
122
|
+
buf.set(counterpartyPk.slice(0, 32), 84);
|
|
123
|
+
view.setUint32(116, circuitVersion >>> 0, true);
|
|
124
|
+
return buf;
|
|
125
|
+
}
|
|
126
|
+
function deriveEncryptionKey(sharedSecret, commitment) {
|
|
127
|
+
const h = import_sha2.sha256.create();
|
|
128
|
+
h.update(sharedSecret);
|
|
129
|
+
h.update(commitment);
|
|
130
|
+
h.update(KEY_DOMAIN);
|
|
131
|
+
return h.digest();
|
|
132
|
+
}
|
|
133
|
+
function deriveViewTag(sharedSecret) {
|
|
134
|
+
const h = import_sha2.sha256.create();
|
|
135
|
+
h.update(VIEW_TAG_DOMAIN);
|
|
136
|
+
h.update(sharedSecret);
|
|
137
|
+
return h.digest()[0];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/protocol/memo/EncryptedMemo.ts
|
|
141
|
+
var NONCE_SIZE = 12;
|
|
142
|
+
var CIPHERTEXT_SIZE = 136;
|
|
143
|
+
var EPH_PK_SIZE = 32;
|
|
144
|
+
var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
|
|
145
|
+
function bytesToBjjScalar(bytes) {
|
|
146
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
147
|
+
return BigInt("0x" + hex) % BABYJUB_SUBORDER || 1n;
|
|
148
|
+
}
|
|
149
|
+
function parsePlaintext(nonce, ciphertextWithMac, encKey) {
|
|
150
|
+
try {
|
|
151
|
+
const cipher = (0, import_chacha.chacha20poly1305)(encKey, nonce);
|
|
152
|
+
const plaintext = cipher.decrypt(ciphertextWithMac);
|
|
153
|
+
const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
|
|
154
|
+
const valueLo = view.getBigUint64(0, true);
|
|
155
|
+
const valueHi = view.getBigUint64(8, true);
|
|
156
|
+
const value = valueLo | valueHi << 64n;
|
|
157
|
+
const ownerPk = bytesToBigintLE(plaintext.slice(16, 48));
|
|
158
|
+
const blinding = bytesToBigintLE(plaintext.slice(48, 80));
|
|
159
|
+
const assetId = BigInt(view.getUint32(80, true));
|
|
160
|
+
const counterpartyPk = bytesToBigintLE(plaintext.slice(84, 116));
|
|
161
|
+
const circuitVersion = view.getUint32(116, true);
|
|
162
|
+
return { value, ownerPk, blinding, assetId, counterpartyPk, circuitVersion };
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
var EncryptedMemo = {
|
|
168
|
+
/**
|
|
169
|
+
* Build and encrypt a memo for a note using ECDH (v2, 180 bytes).
|
|
170
|
+
*
|
|
171
|
+
* @param value Note value in planck.
|
|
172
|
+
* @param ownerPk 32-byte owner public key (LE).
|
|
173
|
+
* @param blinding 32-byte blinding scalar (LE).
|
|
174
|
+
* @param assetId Asset identifier.
|
|
175
|
+
* @param commitment 32-byte commitment bytes (LE).
|
|
176
|
+
* @param recipientIvkPacked 32-byte LE-encoded packed BJJ viewing public key
|
|
177
|
+
* (from PrivacyKeyManager.getViewingPublicKeyPacked() or
|
|
178
|
+
* decoded from a privacy address).
|
|
179
|
+
* Pass `new Uint8Array(32)` (all zeros) for a publicly-readable memo.
|
|
180
|
+
* @param counterpartyPk 32-byte counterparty BJJ Ax. Default: all zeros.
|
|
181
|
+
* @param circuitVersion ZK circuit version the note is spent under. Default: 0.
|
|
182
|
+
* @param ephSkOverride 32-byte ephemeral secret key (stealth coordination). Optional.
|
|
183
|
+
* @returns 180-byte encrypted memo: nonce(12) || ciphertext+MAC(136) || ephPk(32).
|
|
184
|
+
*/
|
|
185
|
+
encrypt(value, ownerPk, blinding, assetId, commitment, recipientIvkPacked, counterpartyPk = new Uint8Array(32), circuitVersion = 0, ephSkOverride) {
|
|
186
|
+
const nonce = (0, import_utils.randomBytes)(NONCE_SIZE);
|
|
187
|
+
const plaintext = serializeMemo(
|
|
188
|
+
value,
|
|
189
|
+
ownerPk,
|
|
190
|
+
blinding,
|
|
191
|
+
assetId,
|
|
192
|
+
counterpartyPk,
|
|
193
|
+
circuitVersion
|
|
194
|
+
);
|
|
195
|
+
const isZeroKey = recipientIvkPacked.every((b) => b === 0);
|
|
196
|
+
let sharedSecret;
|
|
197
|
+
let ephPkPackedBytes;
|
|
198
|
+
if (isZeroKey) {
|
|
199
|
+
sharedSecret = new Uint8Array(32);
|
|
200
|
+
ephPkPackedBytes = new Uint8Array(EPH_PK_SIZE);
|
|
201
|
+
} else {
|
|
202
|
+
const ephSkBytes = ephSkOverride ?? (0, import_utils.randomBytes)(32);
|
|
203
|
+
if (ephSkBytes.length !== 32)
|
|
204
|
+
throw new Error("EncryptedMemo.encrypt: ephSkOverride must be 32 bytes");
|
|
205
|
+
const ephSkScalar = bytesToBjjScalar(ephSkBytes);
|
|
206
|
+
const ephPkPoint = fastMulBase(ephSkScalar);
|
|
207
|
+
ephPkPackedBytes = bigintTo32Le((0, import_baby_jubjub.packPoint)(ephPkPoint));
|
|
208
|
+
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
209
|
+
const ivkPoint = (0, import_baby_jubjub.unpackPoint)(ivkPackedBigint);
|
|
210
|
+
if (!ivkPoint)
|
|
211
|
+
throw new Error("EncryptedMemo.encrypt: invalid recipient viewing public key");
|
|
212
|
+
const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
|
|
213
|
+
sharedSecret = bigintTo32Le(sharedPoint[0]);
|
|
214
|
+
}
|
|
215
|
+
nonce[0] = deriveViewTag(sharedSecret);
|
|
216
|
+
const encKey = deriveEncryptionKey(sharedSecret, commitment);
|
|
217
|
+
const cipher = (0, import_chacha.chacha20poly1305)(encKey, nonce);
|
|
218
|
+
const ciphertext = cipher.encrypt(plaintext);
|
|
219
|
+
const result = new Uint8Array(ENCRYPTED_MEMO_SIZE);
|
|
220
|
+
result.set(nonce, 0);
|
|
221
|
+
result.set(ciphertext, NONCE_SIZE);
|
|
222
|
+
result.set(ephPkPackedBytes, NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
223
|
+
return result;
|
|
224
|
+
},
|
|
225
|
+
/**
|
|
226
|
+
* Returns a 180-byte public memo encrypted with a zero viewing key.
|
|
227
|
+
* Decryptable by anyone with `decrypt(memo, commitment, new Uint8Array(32))`.
|
|
228
|
+
* Convenience alias for `encrypt(..., new Uint8Array(32))`.
|
|
229
|
+
*/
|
|
230
|
+
encryptPublic(value, ownerPk, blinding, assetId, commitment, circuitVersion = 0) {
|
|
231
|
+
return EncryptedMemo.encrypt(
|
|
232
|
+
value,
|
|
233
|
+
ownerPk,
|
|
234
|
+
blinding,
|
|
235
|
+
assetId,
|
|
236
|
+
commitment,
|
|
237
|
+
new Uint8Array(32),
|
|
238
|
+
new Uint8Array(32),
|
|
239
|
+
circuitVersion
|
|
240
|
+
);
|
|
241
|
+
},
|
|
242
|
+
/**
|
|
243
|
+
* Returns a 180-byte zeroed dummy memo (no information, always valid on-chain).
|
|
244
|
+
*/
|
|
245
|
+
dummy() {
|
|
246
|
+
return new Uint8Array(ENCRYPTED_MEMO_SIZE);
|
|
247
|
+
},
|
|
248
|
+
/**
|
|
249
|
+
* Validates that `bytes` is a properly-sized encrypted memo.
|
|
250
|
+
* Throws an Error if the length is not ENCRYPTED_MEMO_SIZE (180 bytes).
|
|
251
|
+
*
|
|
252
|
+
* Call this at system boundaries (extrinsic builders, precompile encoders)
|
|
253
|
+
* to catch malformed memos before they reach the chain and fail on-chain.
|
|
254
|
+
*
|
|
255
|
+
* @param bytes The memo bytes to validate.
|
|
256
|
+
* @param context Optional context string included in the error (e.g. 'shield', 'output[0]').
|
|
257
|
+
*/
|
|
258
|
+
validate(bytes, context) {
|
|
259
|
+
if (bytes.length !== ENCRYPTED_MEMO_SIZE) {
|
|
260
|
+
const ctx = context ? ` (${context})` : "";
|
|
261
|
+
throw new Error(
|
|
262
|
+
`EncryptedMemo: invalid size${ctx} \u2014 expected ${ENCRYPTED_MEMO_SIZE} bytes, got ${bytes.length}`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
/**
|
|
267
|
+
* Decrypt an on-chain EncryptedMemo using the recipient's viewing secret key.
|
|
268
|
+
* Returns null if decryption fails — wrong key, bad MAC, or malformed memo.
|
|
269
|
+
* Never throws; safe for scan loops.
|
|
270
|
+
*
|
|
271
|
+
* @param memoBytes 180-byte encrypted memo.
|
|
272
|
+
* @param commitment 32-byte note commitment (LE).
|
|
273
|
+
* @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
|
|
274
|
+
*/
|
|
275
|
+
decrypt(memoBytes, commitment, viewingSecretKey) {
|
|
276
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
277
|
+
return EncryptedMemo._decrypt(memoBytes, commitment, viewingSecretKey);
|
|
278
|
+
},
|
|
279
|
+
/**
|
|
280
|
+
* Extract the ECDH shared secret from an encrypted memo using the recipient's viewing secret key.
|
|
281
|
+
*
|
|
282
|
+
* Used by NoteDecryptor to obtain the shared secret needed for stealth address derivation
|
|
283
|
+
* without re-running the full decrypt path. Safe to call on any 180-byte memo.
|
|
284
|
+
*
|
|
285
|
+
* Returns `new Uint8Array(32)` (all zeros) for public/dummy memos (zero ephPk).
|
|
286
|
+
* Returns `null` if the memo is malformed or the ephPk is not a valid BJJ point.
|
|
287
|
+
* Never throws; safe for scan loops.
|
|
288
|
+
*
|
|
289
|
+
* @param memoBytes 180-byte encrypted memo.
|
|
290
|
+
* @param viewingSecretKey 32-byte HKDF viewing secret key from deriveViewingSecretKey().
|
|
291
|
+
*/
|
|
292
|
+
extractSharedSecret(memoBytes, viewingSecretKey) {
|
|
293
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
294
|
+
const ephPkPackedBytes = memoBytes.slice(NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
295
|
+
const ephPkPackedBigint = bytesToBigintLE(ephPkPackedBytes);
|
|
296
|
+
if (ephPkPackedBigint === 0n) {
|
|
297
|
+
return new Uint8Array(32);
|
|
298
|
+
}
|
|
299
|
+
const ephPkPoint = (0, import_baby_jubjub.unpackPoint)(ephPkPackedBigint);
|
|
300
|
+
if (!ephPkPoint) return null;
|
|
301
|
+
const ivskScalar = bytesToBjjScalar(viewingSecretKey);
|
|
302
|
+
const sharedPoint = fastMulPoint(ephPkPoint, ivskScalar);
|
|
303
|
+
return bigintTo32Le(sharedPoint[0]);
|
|
304
|
+
},
|
|
305
|
+
/**
|
|
306
|
+
* Cheap view-tag check: does memo nonce[0] match the tag derived from
|
|
307
|
+
* `sharedSecret`? One SHA256 + one byte compare — no AEAD work.
|
|
308
|
+
*
|
|
309
|
+
* Only meaningful for memos built with view tags (commitments at/after
|
|
310
|
+
* the wallet's tagActivationLeaf): a legacy memo carries a random byte
|
|
311
|
+
* there and would false-negative 255/256 of the time.
|
|
312
|
+
*/
|
|
313
|
+
checkViewTag(memoBytes, sharedSecret) {
|
|
314
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return false;
|
|
315
|
+
return memoBytes[0] === deriveViewTag(sharedSecret);
|
|
316
|
+
},
|
|
317
|
+
/**
|
|
318
|
+
* Decrypt with an already-computed shared secret (from
|
|
319
|
+
* extractSharedSecret), skipping the ECDH. Pair with checkViewTag for the
|
|
320
|
+
* fast scan path: ECDH once → tag check → decrypt only on match.
|
|
321
|
+
*/
|
|
322
|
+
decryptWithSharedSecret(memoBytes, commitment, sharedSecret) {
|
|
323
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
324
|
+
const nonce = memoBytes.slice(0, NONCE_SIZE);
|
|
325
|
+
const ciphertextWithMac = memoBytes.slice(NONCE_SIZE, NONCE_SIZE + CIPHERTEXT_SIZE);
|
|
326
|
+
const encKey = deriveEncryptionKey(sharedSecret, commitment);
|
|
327
|
+
return parsePlaintext(nonce, ciphertextWithMac, encKey);
|
|
328
|
+
},
|
|
329
|
+
/** @internal */
|
|
330
|
+
_decrypt(memoBytes, commitment, viewingSecretKey) {
|
|
331
|
+
const sharedSecret = EncryptedMemo.extractSharedSecret(memoBytes, viewingSecretKey);
|
|
332
|
+
if (!sharedSecret) return null;
|
|
333
|
+
return EncryptedMemo.decryptWithSharedSecret(memoBytes, commitment, sharedSecret);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// src/foundation/crypto/stealth.ts
|
|
338
|
+
var import_sha22 = require("@noble/hashes/sha2.js");
|
|
339
|
+
var import_hkdf = require("@noble/hashes/hkdf.js");
|
|
340
|
+
var import_baby_jubjub2 = require("@zk-kit/baby-jubjub");
|
|
341
|
+
var STEALTH_INFO = new TextEncoder().encode("orbinum-stealth-v1");
|
|
342
|
+
function deriveStealthScalar(sharedSecret, ownerPkBigint) {
|
|
343
|
+
const salt = bigintTo32Le(ownerPkBigint);
|
|
344
|
+
const stealthBytes = (0, import_hkdf.hkdf)(import_sha22.sha256, sharedSecret, salt, STEALTH_INFO, 32);
|
|
345
|
+
return bytesToBigintLE(stealthBytes) % BABYJUB_SUBORDER || 1n;
|
|
346
|
+
}
|
|
347
|
+
function deriveStealthOwnerPk(sharedSecret, ownerPkBigint, ownerPkPoint) {
|
|
348
|
+
const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
|
|
349
|
+
const stealthPt = (0, import_baby_jubjub2.addPoint)((0, import_baby_jubjub2.mulPointEscalar)(import_baby_jubjub2.Base8, stealthScalar), ownerPkPoint);
|
|
350
|
+
return stealthPt[0];
|
|
351
|
+
}
|
|
352
|
+
function deriveStealthSk(sharedSecret, ownerPkBigint, spendingKey) {
|
|
353
|
+
const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
|
|
354
|
+
return (stealthScalar + spendingKey) % BABYJUB_SUBORDER || 1n;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/foundation/crypto/bjj.ts
|
|
358
|
+
var import_baby_jubjub3 = require("@zk-kit/baby-jubjub");
|
|
359
|
+
var BJJ_A = 168700n;
|
|
360
|
+
var BJJ_D = 168696n;
|
|
361
|
+
function _modpow(base, exp, mod) {
|
|
362
|
+
let result = 1n;
|
|
363
|
+
base = base % mod;
|
|
364
|
+
while (exp > 0n) {
|
|
365
|
+
if (exp & 1n) result = result * base % mod;
|
|
366
|
+
exp >>= 1n;
|
|
367
|
+
base = base * base % mod;
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
function _sqrtModP(y2) {
|
|
372
|
+
if (y2 === 0n) return 0n;
|
|
373
|
+
if (_modpow(y2, (BN254_R - 1n) / 2n, BN254_R) !== 1n) return null;
|
|
374
|
+
let s = 0n;
|
|
375
|
+
let q = BN254_R - 1n;
|
|
376
|
+
while ((q & 1n) === 0n) {
|
|
377
|
+
q >>= 1n;
|
|
378
|
+
s++;
|
|
379
|
+
}
|
|
380
|
+
if (s === 1n) return _modpow(y2, (BN254_R + 1n) / 4n, BN254_R);
|
|
381
|
+
let z = 2n;
|
|
382
|
+
while (_modpow(z, (BN254_R - 1n) / 2n, BN254_R) === 1n) z++;
|
|
383
|
+
let m = s;
|
|
384
|
+
let c = _modpow(z, q, BN254_R);
|
|
385
|
+
let t = _modpow(y2, q, BN254_R);
|
|
386
|
+
let r = _modpow(y2, (q + 1n) / 2n, BN254_R);
|
|
387
|
+
for (; ; ) {
|
|
388
|
+
if (t === 1n) return r;
|
|
389
|
+
let i = 1n;
|
|
390
|
+
let tmp = t * t % BN254_R;
|
|
391
|
+
while (tmp !== 1n) {
|
|
392
|
+
tmp = tmp * tmp % BN254_R;
|
|
393
|
+
i++;
|
|
394
|
+
}
|
|
395
|
+
const b = _modpow(c, 1n << m - i - 1n, BN254_R);
|
|
396
|
+
m = i;
|
|
397
|
+
c = b * b % BN254_R;
|
|
398
|
+
t = t * c % BN254_R;
|
|
399
|
+
r = r * b % BN254_R;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function recoverOwnerPkPoint(ax) {
|
|
403
|
+
const x2 = ax * ax % BN254_R;
|
|
404
|
+
const num = ((1n - BJJ_A * x2) % BN254_R + BN254_R) % BN254_R;
|
|
405
|
+
const den = ((1n - BJJ_D * x2) % BN254_R + BN254_R) % BN254_R;
|
|
406
|
+
if (den === 0n) return null;
|
|
407
|
+
const denInv = _modpow(den, BN254_R - 2n, BN254_R);
|
|
408
|
+
const y2 = num * denInv % BN254_R;
|
|
409
|
+
const y = _sqrtModP(y2);
|
|
410
|
+
if (y === null) return null;
|
|
411
|
+
const yAlt = BN254_R - y;
|
|
412
|
+
try {
|
|
413
|
+
const check = (0, import_baby_jubjub3.mulPointEscalar)([ax, y], BABYJUB_SUBORDER);
|
|
414
|
+
return check[0] === 0n && check[1] === 1n ? [ax, y] : [ax, yAlt];
|
|
415
|
+
} catch {
|
|
416
|
+
return [ax, yAlt];
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/protocol/note/NoteDecryptor.ts
|
|
421
|
+
var import_poseidon_lite = require("poseidon-lite");
|
|
422
|
+
|
|
423
|
+
// src/protocol/spend/coinSelection.ts
|
|
424
|
+
var TRANSFER_TREE_DEPTH = 20;
|
|
425
|
+
var LEAVES_PER_TREE = 1 << TRANSFER_TREE_DEPTH;
|
|
426
|
+
function isValidLeafIndex(leafIndex) {
|
|
427
|
+
return leafIndex !== null && leafIndex !== void 0 && Number.isSafeInteger(leafIndex) && leafIndex >= 0 && leafIndex < 2 ** 32;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/protocol/note/NoteDecryptor.ts
|
|
431
|
+
function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwnerPk = 0n, opts) {
|
|
432
|
+
if (!commitment.encryptedMemo) return { note: null, reason: "no_memo" };
|
|
433
|
+
let commitmentBytes;
|
|
434
|
+
let memoBytes;
|
|
435
|
+
try {
|
|
436
|
+
commitmentBytes = fromHex(commitment.commitmentHex);
|
|
437
|
+
memoBytes = fromHex(commitment.encryptedMemo);
|
|
438
|
+
} catch {
|
|
439
|
+
return { note: null, reason: "hex_parse_error" };
|
|
440
|
+
}
|
|
441
|
+
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) {
|
|
442
|
+
return {
|
|
443
|
+
note: null,
|
|
444
|
+
reason: `memo_size_mismatch:got_${memoBytes.length}_expected_${ENCRYPTED_MEMO_SIZE}`
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
let sharedSecret = opts?.sharedSecret ?? null;
|
|
448
|
+
if (!sharedSecret && opts?.viewTag) {
|
|
449
|
+
sharedSecret = EncryptedMemo.extractSharedSecret(memoBytes, viewingSecretKey);
|
|
450
|
+
if (!sharedSecret) return { note: null, reason: "stealth_shared_secret_failed" };
|
|
451
|
+
if (!EncryptedMemo.checkViewTag(memoBytes, sharedSecret)) {
|
|
452
|
+
return { note: null, reason: "view_tag_mismatch" };
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const plaintext = sharedSecret ? EncryptedMemo.decryptWithSharedSecret(memoBytes, commitmentBytes, sharedSecret) : EncryptedMemo.decrypt(memoBytes, commitmentBytes, viewingSecretKey);
|
|
456
|
+
if (!plaintext) return { note: null, reason: "decrypt_failed:wrong_key_or_corrupt_mac" };
|
|
457
|
+
let effectiveOwnerPk = plaintext.ownerPk;
|
|
458
|
+
let effectiveSpendingKey = spendingKey;
|
|
459
|
+
if (ownOwnerPk !== 0n && plaintext.ownerPk !== ownOwnerPk) {
|
|
460
|
+
const ss = sharedSecret ?? EncryptedMemo.extractSharedSecret(memoBytes, viewingSecretKey);
|
|
461
|
+
if (!ss) return { note: null, reason: "stealth_shared_secret_failed" };
|
|
462
|
+
const ownPkPoint = recoverOwnerPkPoint(ownOwnerPk);
|
|
463
|
+
if (!ownPkPoint) return { note: null, reason: "stealth_invalid_own_owner_pk" };
|
|
464
|
+
const stealthOwnerPk = deriveStealthOwnerPk(ss, ownOwnerPk, ownPkPoint);
|
|
465
|
+
if (stealthOwnerPk !== plaintext.ownerPk) {
|
|
466
|
+
return { note: null, reason: "commitment_mismatch" };
|
|
467
|
+
}
|
|
468
|
+
effectiveOwnerPk = stealthOwnerPk;
|
|
469
|
+
effectiveSpendingKey = deriveStealthSk(ss, ownOwnerPk, spendingKey);
|
|
470
|
+
}
|
|
471
|
+
const recomputed = (0, import_poseidon_lite.poseidon4)([
|
|
472
|
+
plaintext.value,
|
|
473
|
+
plaintext.assetId,
|
|
474
|
+
effectiveOwnerPk,
|
|
475
|
+
plaintext.blinding
|
|
476
|
+
]);
|
|
477
|
+
if (recomputed !== bytesToBigintLE(commitmentBytes)) {
|
|
478
|
+
return { note: null, reason: "commitment_mismatch" };
|
|
479
|
+
}
|
|
480
|
+
const nullifier = (0, import_poseidon_lite.poseidon2)([recomputed, effectiveSpendingKey]);
|
|
481
|
+
return {
|
|
482
|
+
note: {
|
|
483
|
+
value: plaintext.value,
|
|
484
|
+
assetId: plaintext.assetId,
|
|
485
|
+
ownerPk: effectiveOwnerPk,
|
|
486
|
+
blinding: plaintext.blinding,
|
|
487
|
+
spendingKey: effectiveSpendingKey,
|
|
488
|
+
circuitVersion: plaintext.circuitVersion,
|
|
489
|
+
...isValidLeafIndex(commitment.leafIndex) ? { leafIndex: commitment.leafIndex } : {},
|
|
490
|
+
spent: false,
|
|
491
|
+
spentAt: null,
|
|
492
|
+
commitment: recomputed,
|
|
493
|
+
nullifier,
|
|
494
|
+
commitmentHex: toHex(bigintTo32Le(recomputed)),
|
|
495
|
+
nullifierHex: toHex(bigintTo32Le(nullifier)),
|
|
496
|
+
memo: Array.from(memoBytes),
|
|
497
|
+
counterpartyPk: plaintext.counterpartyPk
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// src/protocol/eph/selfEph.ts
|
|
503
|
+
var import_sha23 = require("@noble/hashes/sha2.js");
|
|
504
|
+
var import_baby_jubjub4 = require("@zk-kit/baby-jubjub");
|
|
505
|
+
var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-v1");
|
|
506
|
+
function deriveSelfEphSk(spendingKey, index) {
|
|
507
|
+
const h = import_sha23.sha256.create();
|
|
508
|
+
h.update(SELF_EPH_DOMAIN);
|
|
509
|
+
h.update(bigintTo32Le(spendingKey));
|
|
510
|
+
const idx = new Uint8Array(4);
|
|
511
|
+
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
512
|
+
h.update(idx);
|
|
513
|
+
return h.digest();
|
|
514
|
+
}
|
|
515
|
+
function selfEphWindow(spendingKey, ivkPacked, from, count) {
|
|
516
|
+
const ivkPoint = (0, import_baby_jubjub4.unpackPoint)(bytesToBigintLE(ivkPacked));
|
|
517
|
+
if (!ivkPoint) throw new Error("selfEphWindow: invalid viewing public key");
|
|
518
|
+
const entries = [];
|
|
519
|
+
for (let i = from; i < from + count; i++) {
|
|
520
|
+
const scalar = bytesToBjjScalar(deriveSelfEphSk(spendingKey, i));
|
|
521
|
+
const ephPk = fastMulBase(scalar);
|
|
522
|
+
const sharedPoint = fastMulPoint(ivkPoint, scalar);
|
|
523
|
+
entries.push({
|
|
524
|
+
index: i,
|
|
525
|
+
ephPkHex: toHex(bigintTo32Le((0, import_baby_jubjub4.packPoint)(ephPk))),
|
|
526
|
+
sharedSecret: bigintTo32Le(sharedPoint[0])
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return entries;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// src/protocol/eph/pairwiseEph.ts
|
|
533
|
+
var import_sha24 = require("@noble/hashes/sha2.js");
|
|
534
|
+
var import_baby_jubjub5 = require("@zk-kit/baby-jubjub");
|
|
535
|
+
var PAIRWISE_EPH_DOMAIN = new TextEncoder().encode("orbinum-pairwise-eph-v1");
|
|
536
|
+
function derivePairwiseSharedSecret(myViewingSk, theirIvkPacked) {
|
|
537
|
+
const theirPoint = (0, import_baby_jubjub5.unpackPoint)(bytesToBigintLE(theirIvkPacked));
|
|
538
|
+
if (!theirPoint) throw new Error("derivePairwiseSharedSecret: invalid viewing public key");
|
|
539
|
+
const shared = fastMulPoint(theirPoint, bytesToBjjScalar(myViewingSk));
|
|
540
|
+
return bigintTo32Le(shared[0]);
|
|
541
|
+
}
|
|
542
|
+
function derivePairwiseEphSk(pairSecret, index) {
|
|
543
|
+
const h = import_sha24.sha256.create();
|
|
544
|
+
h.update(PAIRWISE_EPH_DOMAIN);
|
|
545
|
+
h.update(pairSecret);
|
|
546
|
+
const idx = new Uint8Array(4);
|
|
547
|
+
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
548
|
+
h.update(idx);
|
|
549
|
+
return h.digest();
|
|
550
|
+
}
|
|
551
|
+
function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
|
|
552
|
+
const ivkPoint = (0, import_baby_jubjub5.unpackPoint)(bytesToBigintLE(receiverIvkPacked));
|
|
553
|
+
if (!ivkPoint) throw new Error("pairwiseEphWindow: invalid viewing public key");
|
|
554
|
+
const entries = [];
|
|
555
|
+
for (let i = from; i < from + count; i++) {
|
|
556
|
+
const scalar = bytesToBjjScalar(derivePairwiseEphSk(pairSecret, i));
|
|
557
|
+
const ephPk = fastMulBase(scalar);
|
|
558
|
+
const sharedPoint = fastMulPoint(ivkPoint, scalar);
|
|
559
|
+
entries.push({
|
|
560
|
+
index: i,
|
|
561
|
+
ephPkHex: toHex(bigintTo32Le((0, import_baby_jubjub5.packPoint)(ephPk))),
|
|
562
|
+
sharedSecret: bigintTo32Le(sharedPoint[0])
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return entries;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/protocol/keys/PrivacyKeys.ts
|
|
569
|
+
var import_hkdf2 = require("@noble/hashes/hkdf.js");
|
|
570
|
+
var import_sha25 = require("@noble/hashes/sha2.js");
|
|
571
|
+
var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
|
|
572
|
+
var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
|
|
573
|
+
function deriveViewingPublicKey(ivsk) {
|
|
574
|
+
const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
|
|
575
|
+
const ivkPoint = fastMulBase(ivskScalar);
|
|
576
|
+
const packed = (0, import_baby_jubjub6.packPoint)(ivkPoint);
|
|
577
|
+
return bigintTo32Le(packed);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/wallet/worker/kernel/types.ts
|
|
581
|
+
var SELF_EPH_WINDOW = 1024;
|
|
582
|
+
var PAIRWISE_EPH_WINDOW = 64;
|
|
583
|
+
var EMPTY_BATCH_RESULT = {
|
|
584
|
+
notes: [],
|
|
585
|
+
tagFiltered: 0,
|
|
586
|
+
selfMatched: 0,
|
|
587
|
+
pairwiseMatched: 0,
|
|
588
|
+
maxSelfEphIndex: null
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
// src/wallet/worker/kernel/ephWindow.ts
|
|
592
|
+
var cachedWindow = null;
|
|
593
|
+
function clearKnownEphWindow() {
|
|
594
|
+
cachedWindow = null;
|
|
595
|
+
}
|
|
596
|
+
function bytesKey(bytes) {
|
|
597
|
+
let out = "";
|
|
598
|
+
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
599
|
+
return out;
|
|
600
|
+
}
|
|
601
|
+
function getKnownEphWindow(keys) {
|
|
602
|
+
const selfSize = keys.selfEphWindowSize ?? SELF_EPH_WINDOW;
|
|
603
|
+
const pairSize = keys.pairwiseWindowSize ?? PAIRWISE_EPH_WINDOW;
|
|
604
|
+
const counterparties = keys.pairwiseCounterparties ?? [];
|
|
605
|
+
if (!keys.selfEph && counterparties.length === 0) return null;
|
|
606
|
+
const cacheKey = `${keys.spendingKey.toString(16)}:${keys.selfEph ? selfSize : 0}:${pairSize}:` + counterparties.map((c) => bytesKey(c)).join(",");
|
|
607
|
+
if (cachedWindow?.cacheKey === cacheKey) return cachedWindow.window;
|
|
608
|
+
try {
|
|
609
|
+
const ivkPacked = deriveViewingPublicKey(keys.viewingKey);
|
|
610
|
+
const byEphPk = /* @__PURE__ */ new Map();
|
|
611
|
+
if (keys.selfEph) {
|
|
612
|
+
for (const e of selfEphWindow(keys.spendingKey, ivkPacked, 0, selfSize)) {
|
|
613
|
+
byEphPk.set(e.ephPkHex.toLowerCase(), {
|
|
614
|
+
sharedSecret: e.sharedSecret,
|
|
615
|
+
index: e.index,
|
|
616
|
+
source: "self"
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
for (const theirIvk of counterparties) {
|
|
621
|
+
const pairSecret = derivePairwiseSharedSecret(keys.viewingKey, theirIvk);
|
|
622
|
+
for (const e of pairwiseEphWindow(pairSecret, ivkPacked, 0, pairSize)) {
|
|
623
|
+
const hex = e.ephPkHex.toLowerCase();
|
|
624
|
+
if (!byEphPk.has(hex)) {
|
|
625
|
+
byEphPk.set(hex, {
|
|
626
|
+
sharedSecret: e.sharedSecret,
|
|
627
|
+
index: e.index,
|
|
628
|
+
source: "pairwise"
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
cachedWindow = { cacheKey, window: { byEphPk } };
|
|
634
|
+
return cachedWindow.window;
|
|
635
|
+
} catch {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/wallet/worker/kernel/decryptBatch.ts
|
|
641
|
+
function hintEphPkHex(hint) {
|
|
642
|
+
if (hint.ephPkHex) return hint.ephPkHex.toLowerCase();
|
|
643
|
+
const memo = hint.encryptedMemo;
|
|
644
|
+
if (!memo || memo.length < 66) return null;
|
|
645
|
+
return ("0x" + memo.slice(-64)).toLowerCase();
|
|
646
|
+
}
|
|
647
|
+
function decryptHintBatch(hints, keys) {
|
|
648
|
+
const activation = keys.viewTagActivationLeaf ?? null;
|
|
649
|
+
const knownWindow = getKnownEphWindow(keys);
|
|
650
|
+
let tagFiltered = 0;
|
|
651
|
+
let selfMatched = 0;
|
|
652
|
+
let pairwiseMatched = 0;
|
|
653
|
+
let maxSelfEphIndex = null;
|
|
654
|
+
const notes = hints.map((hint) => {
|
|
655
|
+
try {
|
|
656
|
+
const known = knownWindow?.byEphPk.get(hintEphPkHex(hint) ?? "");
|
|
657
|
+
if (known) {
|
|
658
|
+
const result2 = tryDecryptNoteVerbose(
|
|
659
|
+
hint,
|
|
660
|
+
keys.viewingKey,
|
|
661
|
+
keys.spendingKey,
|
|
662
|
+
keys.ownerPk,
|
|
663
|
+
{ sharedSecret: known.sharedSecret }
|
|
664
|
+
);
|
|
665
|
+
if (result2.note) {
|
|
666
|
+
if (known.source === "self") {
|
|
667
|
+
selfMatched++;
|
|
668
|
+
if (maxSelfEphIndex === null || known.index > maxSelfEphIndex) {
|
|
669
|
+
maxSelfEphIndex = known.index;
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
pairwiseMatched++;
|
|
673
|
+
}
|
|
674
|
+
return result2.note;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
const viewTag = activation !== null && hint.leafIndex >= activation;
|
|
678
|
+
const result = tryDecryptNoteVerbose(
|
|
679
|
+
hint,
|
|
680
|
+
keys.viewingKey,
|
|
681
|
+
keys.spendingKey,
|
|
682
|
+
keys.ownerPk,
|
|
683
|
+
{ viewTag }
|
|
684
|
+
);
|
|
685
|
+
if (result.reason === "view_tag_mismatch") tagFiltered++;
|
|
686
|
+
return result.note;
|
|
687
|
+
} catch {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
return { notes, tagFiltered, selfMatched, pairwiseMatched, maxSelfEphIndex };
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// src/foundation/errors/abort.ts
|
|
695
|
+
function scanAbortError() {
|
|
696
|
+
if (typeof DOMException !== "undefined") {
|
|
697
|
+
return new DOMException("Scan aborted", "AbortError");
|
|
698
|
+
}
|
|
699
|
+
const err = new Error("Scan aborted");
|
|
700
|
+
err.name = "AbortError";
|
|
701
|
+
return err;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/wallet/worker/pool/types.ts
|
|
705
|
+
var MAX_WORKERS = 4;
|
|
706
|
+
var DECRYPT_YIELD_EVERY = 25;
|
|
707
|
+
var WORKER_CRASHED = "Decrypt worker crashed";
|
|
708
|
+
|
|
709
|
+
// src/wallet/worker/pool/mainThreadPool.ts
|
|
710
|
+
function yieldToBrowser() {
|
|
711
|
+
const scheduler = globalThis.scheduler;
|
|
712
|
+
if (scheduler?.yield) return scheduler.yield();
|
|
713
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
714
|
+
}
|
|
715
|
+
function createMainThreadPool() {
|
|
716
|
+
return {
|
|
717
|
+
async decryptBatch(hints, keys, signal) {
|
|
718
|
+
const notes = [];
|
|
719
|
+
let tagFiltered = 0;
|
|
720
|
+
let selfMatched = 0;
|
|
721
|
+
let pairwiseMatched = 0;
|
|
722
|
+
let maxSelfEphIndex = null;
|
|
723
|
+
for (let i = 0; i < hints.length; i += DECRYPT_YIELD_EVERY) {
|
|
724
|
+
if (i > 0) {
|
|
725
|
+
await yieldToBrowser();
|
|
726
|
+
if (signal?.aborted) throw scanAbortError();
|
|
727
|
+
}
|
|
728
|
+
const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY), keys);
|
|
729
|
+
notes.push(...burst.notes);
|
|
730
|
+
tagFiltered += burst.tagFiltered;
|
|
731
|
+
selfMatched += burst.selfMatched;
|
|
732
|
+
pairwiseMatched += burst.pairwiseMatched;
|
|
733
|
+
if (burst.maxSelfEphIndex !== null) {
|
|
734
|
+
maxSelfEphIndex = Math.max(maxSelfEphIndex ?? -1, burst.maxSelfEphIndex);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return { notes, tagFiltered, selfMatched, pairwiseMatched, maxSelfEphIndex };
|
|
738
|
+
},
|
|
739
|
+
terminate() {
|
|
740
|
+
clearKnownEphWindow();
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/wallet/worker/pool/workerRpc.ts
|
|
746
|
+
function runOnWorker(worker, payload, signal) {
|
|
747
|
+
return new Promise((resolve, reject) => {
|
|
748
|
+
const onAbort = () => {
|
|
749
|
+
cleanup();
|
|
750
|
+
reject(scanAbortError());
|
|
751
|
+
};
|
|
752
|
+
const cleanup = () => {
|
|
753
|
+
worker.onmessage = null;
|
|
754
|
+
worker.onerror = null;
|
|
755
|
+
signal?.removeEventListener("abort", onAbort);
|
|
756
|
+
};
|
|
757
|
+
worker.onmessage = (event) => {
|
|
758
|
+
cleanup();
|
|
759
|
+
const data = event.data;
|
|
760
|
+
if (data.error !== void 0) {
|
|
761
|
+
reject(new Error(data.error));
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
resolve({
|
|
765
|
+
notes: data.notes ?? [],
|
|
766
|
+
tagFiltered: data.tagFiltered ?? 0,
|
|
767
|
+
selfMatched: data.selfMatched ?? 0,
|
|
768
|
+
pairwiseMatched: data.pairwiseMatched ?? 0,
|
|
769
|
+
maxSelfEphIndex: data.maxSelfEphIndex ?? null
|
|
770
|
+
});
|
|
771
|
+
};
|
|
772
|
+
worker.onerror = () => {
|
|
773
|
+
cleanup();
|
|
774
|
+
reject(new Error(WORKER_CRASHED));
|
|
775
|
+
};
|
|
776
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
777
|
+
worker.postMessage(payload);
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/wallet/worker/pool/workerPool.ts
|
|
782
|
+
function sliceForWorkers(hints, size) {
|
|
783
|
+
const sliceLength = Math.ceil(hints.length / size);
|
|
784
|
+
const slices = [];
|
|
785
|
+
for (let i = 0; i < hints.length; i += sliceLength) {
|
|
786
|
+
slices.push(hints.slice(i, i + sliceLength));
|
|
787
|
+
}
|
|
788
|
+
return slices;
|
|
789
|
+
}
|
|
790
|
+
function mergeResults(results) {
|
|
791
|
+
return {
|
|
792
|
+
notes: results.flatMap((r) => r.notes),
|
|
793
|
+
tagFiltered: results.reduce((sum, r) => sum + r.tagFiltered, 0),
|
|
794
|
+
selfMatched: results.reduce((sum, r) => sum + r.selfMatched, 0),
|
|
795
|
+
pairwiseMatched: results.reduce((sum, r) => sum + r.pairwiseMatched, 0),
|
|
796
|
+
maxSelfEphIndex: results.reduce(
|
|
797
|
+
(max, r) => r.maxSelfEphIndex === null ? max : Math.max(max ?? -1, r.maxSelfEphIndex),
|
|
798
|
+
null
|
|
799
|
+
)
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function createWorkerPool(factory, size) {
|
|
803
|
+
const workers = [];
|
|
804
|
+
const workerAt = (i) => workers[i] ??= factory();
|
|
805
|
+
return {
|
|
806
|
+
async decryptBatch(hints, keys, signal) {
|
|
807
|
+
if (signal?.aborted) throw scanAbortError();
|
|
808
|
+
if (hints.length === 0) return { ...EMPTY_BATCH_RESULT, notes: [] };
|
|
809
|
+
try {
|
|
810
|
+
const results = await Promise.all(
|
|
811
|
+
sliceForWorkers(hints, size).map(
|
|
812
|
+
(slice, i) => runOnWorker(workerAt(i), { hints: slice, keys }, signal)
|
|
813
|
+
)
|
|
814
|
+
);
|
|
815
|
+
return mergeResults(results);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
this.terminate();
|
|
818
|
+
throw err;
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
terminate() {
|
|
822
|
+
for (const worker of workers.splice(0)) worker.terminate();
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/wallet/worker/pool/createDecryptPool.ts
|
|
828
|
+
function createDecryptPool(options) {
|
|
829
|
+
const { factory } = options;
|
|
830
|
+
if (!factory) return createMainThreadPool();
|
|
831
|
+
let pool = createWorkerPool(factory, Math.min(options.size ?? MAX_WORKERS, MAX_WORKERS));
|
|
832
|
+
return {
|
|
833
|
+
async decryptBatch(hints, keys, signal) {
|
|
834
|
+
try {
|
|
835
|
+
return await pool.decryptBatch(hints, keys, signal);
|
|
836
|
+
} catch (err) {
|
|
837
|
+
if (!(err instanceof Error) || err.message !== WORKER_CRASHED) throw err;
|
|
838
|
+
pool = createMainThreadPool();
|
|
839
|
+
return pool.decryptBatch(hints, keys, signal);
|
|
840
|
+
}
|
|
841
|
+
},
|
|
842
|
+
terminate: () => pool.terminate()
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
846
|
+
0 && (module.exports = {
|
|
847
|
+
DECRYPT_YIELD_EVERY,
|
|
848
|
+
EMPTY_BATCH_RESULT,
|
|
849
|
+
MAX_WORKERS,
|
|
850
|
+
PAIRWISE_EPH_WINDOW,
|
|
851
|
+
SELF_EPH_WINDOW,
|
|
852
|
+
WORKER_CRASHED,
|
|
853
|
+
clearKnownEphWindow,
|
|
854
|
+
createDecryptPool,
|
|
855
|
+
createMainThreadPool,
|
|
856
|
+
createWorkerPool,
|
|
857
|
+
decryptHintBatch,
|
|
858
|
+
getKnownEphWindow
|
|
859
|
+
});
|