@orbinum/sdk 2.0.0 → 3.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 +15 -18
- package/dist/adapters/indexeddb/index.d.mts +10 -10
- package/dist/adapters/indexeddb/index.d.ts +10 -10
- package/dist/{chunk-VYKKBXOE.mjs → chunk-54HFH63J.mjs} +493 -256
- package/dist/{index-CLpM1984.d.mts → index-B-dbSIPN.d.mts} +209 -52
- package/dist/{index-CLpM1984.d.ts → index-B-dbSIPN.d.ts} +209 -52
- package/dist/index.d.mts +977 -498
- package/dist/index.d.ts +977 -498
- package/dist/index.js +990 -512
- package/dist/index.mjs +572 -313
- package/dist/{secretStore-CF6Nse__.d.ts → secretStore-CCYo4hna.d.mts} +15 -11
- package/dist/{secretStore-CF6Nse__.d.mts → secretStore-CCYo4hna.d.ts} +15 -11
- package/dist/wallet/worker/index.d.mts +1 -1
- package/dist/wallet/worker/index.d.ts +1 -1
- package/dist/wallet/worker/index.js +428 -111
- package/dist/wallet/worker/index.mjs +3 -1
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ __export(worker_exports, {
|
|
|
23
23
|
DECRYPT_YIELD_EVERY: () => DECRYPT_YIELD_EVERY,
|
|
24
24
|
EMPTY_BATCH_RESULT: () => EMPTY_BATCH_RESULT,
|
|
25
25
|
MAX_WORKERS: () => MAX_WORKERS,
|
|
26
|
+
OUTGOING_EPH_WINDOW: () => OUTGOING_EPH_WINDOW,
|
|
26
27
|
PAIRWISE_EPH_WINDOW: () => PAIRWISE_EPH_WINDOW,
|
|
27
28
|
SELF_EPH_WINDOW: () => SELF_EPH_WINDOW,
|
|
28
29
|
WORKER_CRASHED: () => WORKER_CRASHED,
|
|
@@ -38,7 +39,7 @@ module.exports = __toCommonJS(worker_exports);
|
|
|
38
39
|
// src/protocol/memo/EncryptedMemo.ts
|
|
39
40
|
var import_chacha = require("@noble/ciphers/chacha.js");
|
|
40
41
|
var import_utils = require("@noble/ciphers/utils.js");
|
|
41
|
-
var
|
|
42
|
+
var import_baby_jubjub2 = require("@zk-kit/baby-jubjub");
|
|
42
43
|
|
|
43
44
|
// src/foundation/crypto/bjj-fast.ts
|
|
44
45
|
var import_edwards = require("@noble/curves/abstract/edwards.js");
|
|
@@ -66,7 +67,88 @@ function fastMulPoint(point, scalar) {
|
|
|
66
67
|
return [x, y];
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
// src/foundation/crypto/bjj.ts
|
|
71
|
+
var import_baby_jubjub = require("@zk-kit/baby-jubjub");
|
|
72
|
+
|
|
73
|
+
// src/foundation/crypto/constants.ts
|
|
74
|
+
var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
75
|
+
var BABYJUB_SUBORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
|
|
76
|
+
|
|
77
|
+
// src/foundation/crypto/bjj.ts
|
|
78
|
+
var BJJ_A = 168700n;
|
|
79
|
+
var BJJ_D = 168696n;
|
|
80
|
+
var BJJ_COFACTOR = 8n;
|
|
81
|
+
function _modpow(base, exp, mod) {
|
|
82
|
+
let result = 1n;
|
|
83
|
+
base = base % mod;
|
|
84
|
+
while (exp > 0n) {
|
|
85
|
+
if (exp & 1n) result = result * base % mod;
|
|
86
|
+
exp >>= 1n;
|
|
87
|
+
base = base * base % mod;
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
function _sqrtModP(y2) {
|
|
92
|
+
if (y2 === 0n) return 0n;
|
|
93
|
+
if (_modpow(y2, (BN254_R - 1n) / 2n, BN254_R) !== 1n) return null;
|
|
94
|
+
let s = 0n;
|
|
95
|
+
let q = BN254_R - 1n;
|
|
96
|
+
while ((q & 1n) === 0n) {
|
|
97
|
+
q >>= 1n;
|
|
98
|
+
s++;
|
|
99
|
+
}
|
|
100
|
+
if (s === 1n) return _modpow(y2, (BN254_R + 1n) / 4n, BN254_R);
|
|
101
|
+
let z = 2n;
|
|
102
|
+
while (_modpow(z, (BN254_R - 1n) / 2n, BN254_R) === 1n) z++;
|
|
103
|
+
let m = s;
|
|
104
|
+
let c = _modpow(z, q, BN254_R);
|
|
105
|
+
let t = _modpow(y2, q, BN254_R);
|
|
106
|
+
let r = _modpow(y2, (q + 1n) / 2n, BN254_R);
|
|
107
|
+
for (; ; ) {
|
|
108
|
+
if (t === 1n) return r;
|
|
109
|
+
let i = 1n;
|
|
110
|
+
let tmp = t * t % BN254_R;
|
|
111
|
+
while (tmp !== 1n) {
|
|
112
|
+
tmp = tmp * tmp % BN254_R;
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
const b = _modpow(c, 1n << m - i - 1n, BN254_R);
|
|
116
|
+
m = i;
|
|
117
|
+
c = b * b % BN254_R;
|
|
118
|
+
t = t * c % BN254_R;
|
|
119
|
+
r = r * b % BN254_R;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function recoverOwnerPkPoint(ax) {
|
|
123
|
+
const x2 = ax * ax % BN254_R;
|
|
124
|
+
const num = ((1n - BJJ_A * x2) % BN254_R + BN254_R) % BN254_R;
|
|
125
|
+
const den = ((1n - BJJ_D * x2) % BN254_R + BN254_R) % BN254_R;
|
|
126
|
+
if (den === 0n) return null;
|
|
127
|
+
const denInv = _modpow(den, BN254_R - 2n, BN254_R);
|
|
128
|
+
const y2 = num * denInv % BN254_R;
|
|
129
|
+
const y = _sqrtModP(y2);
|
|
130
|
+
if (y === null) return null;
|
|
131
|
+
const yAlt = BN254_R - y;
|
|
132
|
+
try {
|
|
133
|
+
const check = (0, import_baby_jubjub.mulPointEscalar)([ax, y], BABYJUB_SUBORDER);
|
|
134
|
+
return check[0] === 0n && check[1] === 1n ? [ax, y] : [ax, yAlt];
|
|
135
|
+
} catch {
|
|
136
|
+
return [ax, yAlt];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function unpackUsableViewingKey(packed) {
|
|
140
|
+
const point = (0, import_baby_jubjub.unpackPoint)(packed);
|
|
141
|
+
if (!point) return null;
|
|
142
|
+
if (point[0] === 0n && point[1] === 1n) return null;
|
|
143
|
+
const cleared = (0, import_baby_jubjub.mulPointEscalar)(point, BJJ_COFACTOR);
|
|
144
|
+
if (cleared[0] === 0n && cleared[1] === 1n) return null;
|
|
145
|
+
return point;
|
|
146
|
+
}
|
|
147
|
+
|
|
69
148
|
// src/foundation/encoding/hex.ts
|
|
149
|
+
function isHexOfLength(value, byteLen) {
|
|
150
|
+
return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${byteLen * 2}}$`).test(value);
|
|
151
|
+
}
|
|
70
152
|
function toHex(bytes) {
|
|
71
153
|
return "0x" + Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
72
154
|
}
|
|
@@ -109,16 +191,21 @@ function bytesToBigintLE(bytes) {
|
|
|
109
191
|
return result;
|
|
110
192
|
}
|
|
111
193
|
|
|
112
|
-
// src/foundation/crypto/constants.ts
|
|
113
|
-
var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
114
|
-
var BABYJUB_SUBORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
|
|
115
|
-
|
|
116
194
|
// src/protocol/memo/plaintext.ts
|
|
117
195
|
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
118
196
|
var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
|
|
119
197
|
var VIEW_TAG_DOMAIN = new TextEncoder().encode("orbinum-view-tag-v1");
|
|
120
198
|
var MEMO_PLAINTEXT_SIZE = 120;
|
|
121
199
|
function serializeMemo(value, ownerPk, blinding, assetId, sourcePk, circuitVersion) {
|
|
200
|
+
if (value < 0n || value >> 128n !== 0n) {
|
|
201
|
+
throw new Error(`serializeMemo: value must fit in 128 unsigned bits, got ${value}`);
|
|
202
|
+
}
|
|
203
|
+
if (!Number.isInteger(assetId) || assetId < 0 || assetId > 4294967295) {
|
|
204
|
+
throw new Error(`serializeMemo: assetId must be a u32, got ${assetId}`);
|
|
205
|
+
}
|
|
206
|
+
if (!Number.isInteger(circuitVersion) || circuitVersion < 0 || circuitVersion > 4294967295) {
|
|
207
|
+
throw new Error(`serializeMemo: circuitVersion must be a u32, got ${circuitVersion}`);
|
|
208
|
+
}
|
|
122
209
|
const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
|
|
123
210
|
const view = new DataView(buf.buffer);
|
|
124
211
|
view.setBigUint64(0, value & 0xffffffffffffffffn, true);
|
|
@@ -150,6 +237,9 @@ var CIPHERTEXT_SIZE = 136;
|
|
|
150
237
|
var EPH_PK_SIZE = 32;
|
|
151
238
|
var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
|
|
152
239
|
function bytesToBjjScalar(bytes) {
|
|
240
|
+
if (bytes.length !== 32) {
|
|
241
|
+
throw new Error(`bytesToBjjScalar: expected 32 bytes, got ${bytes.length}`);
|
|
242
|
+
}
|
|
153
243
|
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
154
244
|
return BigInt("0x" + hex) % BABYJUB_SUBORDER || 1n;
|
|
155
245
|
}
|
|
@@ -173,7 +263,7 @@ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
|
|
|
173
263
|
}
|
|
174
264
|
var EncryptedMemo = {
|
|
175
265
|
/**
|
|
176
|
-
* Build and encrypt a memo for a note using ECDH (
|
|
266
|
+
* Build and encrypt a memo for a note using ECDH (180 bytes).
|
|
177
267
|
*
|
|
178
268
|
* @param value Note value in planck.
|
|
179
269
|
* @param ownerPk 32-byte owner public key (LE).
|
|
@@ -211,9 +301,9 @@ var EncryptedMemo = {
|
|
|
211
301
|
throw new Error("EncryptedMemo.encrypt: ephSkOverride must be 32 bytes");
|
|
212
302
|
const ephSkScalar = bytesToBjjScalar(ephSkBytes);
|
|
213
303
|
const ephPkPoint = fastMulBase(ephSkScalar);
|
|
214
|
-
ephPkPackedBytes = bigintTo32Le((0,
|
|
304
|
+
ephPkPackedBytes = bigintTo32Le((0, import_baby_jubjub2.packPoint)(ephPkPoint));
|
|
215
305
|
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
216
|
-
const ivkPoint = (
|
|
306
|
+
const ivkPoint = unpackUsableViewingKey(ivkPackedBigint);
|
|
217
307
|
if (!ivkPoint)
|
|
218
308
|
throw new Error("EncryptedMemo.encrypt: invalid recipient viewing public key");
|
|
219
309
|
const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
|
|
@@ -303,7 +393,7 @@ var EncryptedMemo = {
|
|
|
303
393
|
if (ephPkPackedBigint === 0n) {
|
|
304
394
|
return new Uint8Array(32);
|
|
305
395
|
}
|
|
306
|
-
const ephPkPoint = (
|
|
396
|
+
const ephPkPoint = unpackUsableViewingKey(ephPkPackedBigint);
|
|
307
397
|
if (!ephPkPoint) return null;
|
|
308
398
|
const ivskScalar = bytesToBjjScalar(viewingSecretKey);
|
|
309
399
|
const sharedPoint = fastMulPoint(ephPkPoint, ivskScalar);
|
|
@@ -313,9 +403,9 @@ var EncryptedMemo = {
|
|
|
313
403
|
* Cheap view-tag check: does memo nonce[0] match the tag derived from
|
|
314
404
|
* `sharedSecret`? One SHA256 + one byte compare — no AEAD work.
|
|
315
405
|
*
|
|
316
|
-
* Only meaningful for memos
|
|
317
|
-
*
|
|
318
|
-
*
|
|
406
|
+
* Only meaningful for memos that carry a tag — at or after
|
|
407
|
+
* `ScanKeys.viewTagActivationLeaf`. An older memo has a random byte there
|
|
408
|
+
* and would false-negative 255/256 of the time.
|
|
319
409
|
*/
|
|
320
410
|
checkViewTag(memoBytes, sharedSecret) {
|
|
321
411
|
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return false;
|
|
@@ -344,7 +434,7 @@ var EncryptedMemo = {
|
|
|
344
434
|
// src/foundation/crypto/stealth.ts
|
|
345
435
|
var import_sha22 = require("@noble/hashes/sha2.js");
|
|
346
436
|
var import_hkdf = require("@noble/hashes/hkdf.js");
|
|
347
|
-
var
|
|
437
|
+
var import_baby_jubjub3 = require("@zk-kit/baby-jubjub");
|
|
348
438
|
var STEALTH_INFO = new TextEncoder().encode("orbinum-stealth-v1");
|
|
349
439
|
function deriveStealthScalar(sharedSecret, ownerPkBigint) {
|
|
350
440
|
const salt = bigintTo32Le(ownerPkBigint);
|
|
@@ -353,7 +443,7 @@ function deriveStealthScalar(sharedSecret, ownerPkBigint) {
|
|
|
353
443
|
}
|
|
354
444
|
function deriveStealthOwnerPk(sharedSecret, ownerPkBigint, ownerPkPoint) {
|
|
355
445
|
const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
|
|
356
|
-
const stealthPt = (0,
|
|
446
|
+
const stealthPt = (0, import_baby_jubjub3.addPoint)((0, import_baby_jubjub3.mulPointEscalar)(import_baby_jubjub3.Base8, stealthScalar), ownerPkPoint);
|
|
357
447
|
return stealthPt[0];
|
|
358
448
|
}
|
|
359
449
|
function deriveStealthSk(sharedSecret, ownerPkBigint, spendingKey) {
|
|
@@ -361,69 +451,6 @@ function deriveStealthSk(sharedSecret, ownerPkBigint, spendingKey) {
|
|
|
361
451
|
return (stealthScalar + spendingKey) % BABYJUB_SUBORDER || 1n;
|
|
362
452
|
}
|
|
363
453
|
|
|
364
|
-
// src/foundation/crypto/bjj.ts
|
|
365
|
-
var import_baby_jubjub3 = require("@zk-kit/baby-jubjub");
|
|
366
|
-
var BJJ_A = 168700n;
|
|
367
|
-
var BJJ_D = 168696n;
|
|
368
|
-
function _modpow(base, exp, mod) {
|
|
369
|
-
let result = 1n;
|
|
370
|
-
base = base % mod;
|
|
371
|
-
while (exp > 0n) {
|
|
372
|
-
if (exp & 1n) result = result * base % mod;
|
|
373
|
-
exp >>= 1n;
|
|
374
|
-
base = base * base % mod;
|
|
375
|
-
}
|
|
376
|
-
return result;
|
|
377
|
-
}
|
|
378
|
-
function _sqrtModP(y2) {
|
|
379
|
-
if (y2 === 0n) return 0n;
|
|
380
|
-
if (_modpow(y2, (BN254_R - 1n) / 2n, BN254_R) !== 1n) return null;
|
|
381
|
-
let s = 0n;
|
|
382
|
-
let q = BN254_R - 1n;
|
|
383
|
-
while ((q & 1n) === 0n) {
|
|
384
|
-
q >>= 1n;
|
|
385
|
-
s++;
|
|
386
|
-
}
|
|
387
|
-
if (s === 1n) return _modpow(y2, (BN254_R + 1n) / 4n, BN254_R);
|
|
388
|
-
let z = 2n;
|
|
389
|
-
while (_modpow(z, (BN254_R - 1n) / 2n, BN254_R) === 1n) z++;
|
|
390
|
-
let m = s;
|
|
391
|
-
let c = _modpow(z, q, BN254_R);
|
|
392
|
-
let t = _modpow(y2, q, BN254_R);
|
|
393
|
-
let r = _modpow(y2, (q + 1n) / 2n, BN254_R);
|
|
394
|
-
for (; ; ) {
|
|
395
|
-
if (t === 1n) return r;
|
|
396
|
-
let i = 1n;
|
|
397
|
-
let tmp = t * t % BN254_R;
|
|
398
|
-
while (tmp !== 1n) {
|
|
399
|
-
tmp = tmp * tmp % BN254_R;
|
|
400
|
-
i++;
|
|
401
|
-
}
|
|
402
|
-
const b = _modpow(c, 1n << m - i - 1n, BN254_R);
|
|
403
|
-
m = i;
|
|
404
|
-
c = b * b % BN254_R;
|
|
405
|
-
t = t * c % BN254_R;
|
|
406
|
-
r = r * b % BN254_R;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
function recoverOwnerPkPoint(ax) {
|
|
410
|
-
const x2 = ax * ax % BN254_R;
|
|
411
|
-
const num = ((1n - BJJ_A * x2) % BN254_R + BN254_R) % BN254_R;
|
|
412
|
-
const den = ((1n - BJJ_D * x2) % BN254_R + BN254_R) % BN254_R;
|
|
413
|
-
if (den === 0n) return null;
|
|
414
|
-
const denInv = _modpow(den, BN254_R - 2n, BN254_R);
|
|
415
|
-
const y2 = num * denInv % BN254_R;
|
|
416
|
-
const y = _sqrtModP(y2);
|
|
417
|
-
if (y === null) return null;
|
|
418
|
-
const yAlt = BN254_R - y;
|
|
419
|
-
try {
|
|
420
|
-
const check = (0, import_baby_jubjub3.mulPointEscalar)([ax, y], BABYJUB_SUBORDER);
|
|
421
|
-
return check[0] === 0n && check[1] === 1n ? [ax, y] : [ax, yAlt];
|
|
422
|
-
} catch {
|
|
423
|
-
return [ax, yAlt];
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
454
|
// src/protocol/note/NoteDecryptor.ts
|
|
428
455
|
var import_poseidon_lite = require("poseidon-lite");
|
|
429
456
|
|
|
@@ -506,30 +533,187 @@ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwn
|
|
|
506
533
|
};
|
|
507
534
|
}
|
|
508
535
|
|
|
509
|
-
// src/protocol/eph/
|
|
536
|
+
// src/protocol/eph/outgoingEph.ts
|
|
510
537
|
var import_sha23 = require("@noble/hashes/sha2.js");
|
|
538
|
+
|
|
539
|
+
// src/protocol/eph/windowBounds.ts
|
|
540
|
+
var MAX_EPH_WINDOW = 1 << 20;
|
|
541
|
+
|
|
542
|
+
// src/foundation/crypto/keyGuards.ts
|
|
543
|
+
var SECRET_KEY_SIZE = 32;
|
|
544
|
+
function isUsableSecretKey(key) {
|
|
545
|
+
return key !== void 0 && key !== null && key.length === SECRET_KEY_SIZE && !key.every((b) => b === 0);
|
|
546
|
+
}
|
|
547
|
+
function assertSecretKeyBytes(key, label) {
|
|
548
|
+
if (key.length !== SECRET_KEY_SIZE) {
|
|
549
|
+
throw new Error(`${label} must be ${SECRET_KEY_SIZE} bytes, got ${key.length}`);
|
|
550
|
+
}
|
|
551
|
+
if (key.every((b) => b === 0)) {
|
|
552
|
+
throw new Error(`${label} is all zeros \u2014 that is not key material`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/protocol/eph/outgoingEph.ts
|
|
511
557
|
var import_baby_jubjub4 = require("@zk-kit/baby-jubjub");
|
|
512
|
-
var
|
|
513
|
-
function
|
|
558
|
+
var OUTGOING_EPH_DOMAIN = new TextEncoder().encode("orbinum-outgoing-eph-v3");
|
|
559
|
+
function deriveOutgoingEphSk(outgoingViewingKey, index) {
|
|
560
|
+
if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
|
|
561
|
+
throw new Error(`deriveOutgoingEphSk: index must be a u32, got ${index}`);
|
|
562
|
+
}
|
|
563
|
+
assertSecretKeyBytes(outgoingViewingKey, "deriveOutgoingEphSk: outgoingViewingKey");
|
|
514
564
|
const h = import_sha23.sha256.create();
|
|
565
|
+
h.update(OUTGOING_EPH_DOMAIN);
|
|
566
|
+
h.update(outgoingViewingKey);
|
|
567
|
+
const idx = new Uint8Array(4);
|
|
568
|
+
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
569
|
+
h.update(idx);
|
|
570
|
+
return h.digest();
|
|
571
|
+
}
|
|
572
|
+
function deriveOutgoingEphPk(outgoingViewingKey, index) {
|
|
573
|
+
const scalar = bytesToBjjScalar(deriveOutgoingEphSk(outgoingViewingKey, index));
|
|
574
|
+
return toHex(bigintTo32Le((0, import_baby_jubjub4.packPoint)(fastMulBase(scalar)))).toLowerCase();
|
|
575
|
+
}
|
|
576
|
+
function outgoingEphWindow(outgoingViewingKey, from, count) {
|
|
577
|
+
const entries = [];
|
|
578
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
579
|
+
throw new Error(`outgoingEphWindow: from must be a non-negative integer, got ${from}`);
|
|
580
|
+
}
|
|
581
|
+
if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
`outgoingEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
for (let i = from; i < from + count; i++) {
|
|
587
|
+
entries.push({ index: i, ephPkHex: deriveOutgoingEphPk(outgoingViewingKey, i) });
|
|
588
|
+
}
|
|
589
|
+
return entries;
|
|
590
|
+
}
|
|
591
|
+
function deriveOutgoingSharedSecret(outgoingViewingKey, index, recipientIvkPacked) {
|
|
592
|
+
const ivkPoint = (0, import_baby_jubjub4.unpackPoint)(bytesToBigintLE(recipientIvkPacked));
|
|
593
|
+
if (!ivkPoint) throw new Error("deriveOutgoingSharedSecret: invalid viewing public key");
|
|
594
|
+
const scalar = bytesToBjjScalar(deriveOutgoingEphSk(outgoingViewingKey, index));
|
|
595
|
+
return bigintTo32Le(fastMulPoint(ivkPoint, scalar)[0]);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// src/protocol/note/recoverSent.ts
|
|
599
|
+
function recoverSentFromSharedSecret(hint, outgoingSharedSecret, ephIndex) {
|
|
600
|
+
if (!isHexOfLength(hint.commitmentHex, 32)) return null;
|
|
601
|
+
if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
|
|
602
|
+
try {
|
|
603
|
+
const plaintext = EncryptedMemo.decryptWithSharedSecret(
|
|
604
|
+
fromHex(hint.encryptedMemo),
|
|
605
|
+
fromHex(hint.commitmentHex),
|
|
606
|
+
outgoingSharedSecret
|
|
607
|
+
);
|
|
608
|
+
if (!plaintext) return null;
|
|
609
|
+
return {
|
|
610
|
+
commitmentHex: hint.commitmentHex,
|
|
611
|
+
...isValidLeafIndex(hint.leafIndex) ? { leafIndex: hint.leafIndex } : {},
|
|
612
|
+
value: plaintext.value,
|
|
613
|
+
assetId: plaintext.assetId,
|
|
614
|
+
// On an outgoing note the memo's ownerPk IS the recipient's stealth
|
|
615
|
+
// owner — the builder wrote it that way.
|
|
616
|
+
recipientStealthPk: plaintext.ownerPk,
|
|
617
|
+
blinding: plaintext.blinding,
|
|
618
|
+
sourcePk: plaintext.sourcePk,
|
|
619
|
+
circuitVersion: plaintext.circuitVersion,
|
|
620
|
+
ephIndex
|
|
621
|
+
};
|
|
622
|
+
} catch {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function recoverSentNote(params) {
|
|
627
|
+
const { hint, outgoingViewingKey, ephIndex, recipientCandidates } = params;
|
|
628
|
+
if (!isHexOfLength(hint.commitmentHex, 32)) return null;
|
|
629
|
+
if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
|
|
630
|
+
for (const candidate of recipientCandidates) {
|
|
631
|
+
let sharedSecret;
|
|
632
|
+
try {
|
|
633
|
+
sharedSecret = deriveOutgoingSharedSecret(outgoingViewingKey, ephIndex, candidate);
|
|
634
|
+
} catch {
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const facts = recoverSentFromSharedSecret(hint, sharedSecret, ephIndex);
|
|
638
|
+
if (facts) return { ...facts, recipientIvk: candidate };
|
|
639
|
+
}
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/protocol/note/recipientBook.ts
|
|
644
|
+
var import_sha24 = require("@noble/hashes/sha2.js");
|
|
645
|
+
var RECIPIENT_BOOK_DOMAIN = new TextEncoder().encode("orbinum-recipient-book-v3");
|
|
646
|
+
var IVK_SIZE = 32;
|
|
647
|
+
function bookKeystream(outgoingViewingKey, paymentCommitmentHex) {
|
|
648
|
+
if (outgoingViewingKey.length !== 32) {
|
|
649
|
+
throw new Error(
|
|
650
|
+
`recipientBook: outgoingViewingKey must be 32 bytes, got ${outgoingViewingKey.length}`
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (outgoingViewingKey.every((b) => b === 0)) {
|
|
654
|
+
throw new Error(
|
|
655
|
+
"recipientBook: outgoingViewingKey is all zeros \u2014 the book would be public"
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const normalised = paymentCommitmentHex.toLowerCase().replace(/^0x/, "");
|
|
659
|
+
const h = import_sha24.sha256.create();
|
|
660
|
+
h.update(RECIPIENT_BOOK_DOMAIN);
|
|
661
|
+
h.update(outgoingViewingKey);
|
|
662
|
+
h.update(new TextEncoder().encode(normalised));
|
|
663
|
+
return h.digest();
|
|
664
|
+
}
|
|
665
|
+
function sealRecipientBookEntry(ivkPacked, outgoingViewingKey, paymentCommitmentHex) {
|
|
666
|
+
if (ivkPacked.length !== IVK_SIZE) return ivkPacked;
|
|
667
|
+
const keystream = bookKeystream(outgoingViewingKey, paymentCommitmentHex);
|
|
668
|
+
const out = new Uint8Array(IVK_SIZE);
|
|
669
|
+
for (let i = 0; i < IVK_SIZE; i++) out[i] = ivkPacked[i] ^ keystream[i];
|
|
670
|
+
return out;
|
|
671
|
+
}
|
|
672
|
+
function openRecipientBookEntry(sealedSourcePk, outgoingViewingKey, paymentCommitmentHex) {
|
|
673
|
+
if (sealedSourcePk < 0n || sealedSourcePk >> 256n !== 0n) return new Uint8Array(IVK_SIZE);
|
|
674
|
+
return sealRecipientBookEntry(
|
|
675
|
+
bigintTo32Le(sealedSourcePk),
|
|
676
|
+
outgoingViewingKey,
|
|
677
|
+
paymentCommitmentHex
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/protocol/eph/selfEph.ts
|
|
682
|
+
var import_sha25 = require("@noble/hashes/sha2.js");
|
|
683
|
+
var import_baby_jubjub5 = require("@zk-kit/baby-jubjub");
|
|
684
|
+
var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-v3");
|
|
685
|
+
function deriveSelfEphSk(viewingSecretKey, index) {
|
|
686
|
+
if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
|
|
687
|
+
throw new Error(`deriveSelfEphSk: index must be a u32, got ${index}`);
|
|
688
|
+
}
|
|
689
|
+
assertSecretKeyBytes(viewingSecretKey, "deriveSelfEphSk: viewingSecretKey");
|
|
690
|
+
const h = import_sha25.sha256.create();
|
|
515
691
|
h.update(SELF_EPH_DOMAIN);
|
|
516
|
-
h.update(
|
|
692
|
+
h.update(viewingSecretKey);
|
|
517
693
|
const idx = new Uint8Array(4);
|
|
518
694
|
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
519
695
|
h.update(idx);
|
|
520
696
|
return h.digest();
|
|
521
697
|
}
|
|
522
|
-
function selfEphWindow(
|
|
523
|
-
const ivkPoint = (0,
|
|
698
|
+
function selfEphWindow(viewingSecretKey, ivkPacked, from, count) {
|
|
699
|
+
const ivkPoint = (0, import_baby_jubjub5.unpackPoint)(bytesToBigintLE(ivkPacked));
|
|
524
700
|
if (!ivkPoint) throw new Error("selfEphWindow: invalid viewing public key");
|
|
525
701
|
const entries = [];
|
|
702
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
703
|
+
throw new Error(`selfEphWindow: from must be a non-negative integer, got ${from}`);
|
|
704
|
+
}
|
|
705
|
+
if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`selfEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
526
710
|
for (let i = from; i < from + count; i++) {
|
|
527
|
-
const scalar = bytesToBjjScalar(deriveSelfEphSk(
|
|
711
|
+
const scalar = bytesToBjjScalar(deriveSelfEphSk(viewingSecretKey, i));
|
|
528
712
|
const ephPk = fastMulBase(scalar);
|
|
529
713
|
const sharedPoint = fastMulPoint(ivkPoint, scalar);
|
|
530
714
|
entries.push({
|
|
531
715
|
index: i,
|
|
532
|
-
ephPkHex: toHex(bigintTo32Le((0,
|
|
716
|
+
ephPkHex: toHex(bigintTo32Le((0, import_baby_jubjub5.packPoint)(ephPk))),
|
|
533
717
|
sharedSecret: bigintTo32Le(sharedPoint[0])
|
|
534
718
|
});
|
|
535
719
|
}
|
|
@@ -537,17 +721,22 @@ function selfEphWindow(spendingKey, ivkPacked, from, count) {
|
|
|
537
721
|
}
|
|
538
722
|
|
|
539
723
|
// src/protocol/eph/pairwiseEph.ts
|
|
540
|
-
var
|
|
541
|
-
var
|
|
724
|
+
var import_sha26 = require("@noble/hashes/sha2.js");
|
|
725
|
+
var import_baby_jubjub6 = require("@zk-kit/baby-jubjub");
|
|
542
726
|
var PAIRWISE_EPH_DOMAIN = new TextEncoder().encode("orbinum-pairwise-eph-v1");
|
|
543
727
|
function derivePairwiseSharedSecret(myViewingSk, theirIvkPacked) {
|
|
544
|
-
const theirPoint = (
|
|
728
|
+
const theirPoint = unpackUsableViewingKey(bytesToBigintLE(theirIvkPacked));
|
|
545
729
|
if (!theirPoint) throw new Error("derivePairwiseSharedSecret: invalid viewing public key");
|
|
730
|
+
assertSecretKeyBytes(myViewingSk, "derivePairwiseSharedSecret: myViewingSk");
|
|
546
731
|
const shared = fastMulPoint(theirPoint, bytesToBjjScalar(myViewingSk));
|
|
547
732
|
return bigintTo32Le(shared[0]);
|
|
548
733
|
}
|
|
549
734
|
function derivePairwiseEphSk(pairSecret, index) {
|
|
550
|
-
|
|
735
|
+
if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
|
|
736
|
+
throw new Error(`derivePairwiseEphSk: index must be a u32, got ${index}`);
|
|
737
|
+
}
|
|
738
|
+
assertSecretKeyBytes(pairSecret, "derivePairwiseEphSk: pairSecret");
|
|
739
|
+
const h = import_sha26.sha256.create();
|
|
551
740
|
h.update(PAIRWISE_EPH_DOMAIN);
|
|
552
741
|
h.update(pairSecret);
|
|
553
742
|
const idx = new Uint8Array(4);
|
|
@@ -556,16 +745,24 @@ function derivePairwiseEphSk(pairSecret, index) {
|
|
|
556
745
|
return h.digest();
|
|
557
746
|
}
|
|
558
747
|
function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
|
|
559
|
-
const ivkPoint = (0,
|
|
748
|
+
const ivkPoint = (0, import_baby_jubjub6.unpackPoint)(bytesToBigintLE(receiverIvkPacked));
|
|
560
749
|
if (!ivkPoint) throw new Error("pairwiseEphWindow: invalid viewing public key");
|
|
561
750
|
const entries = [];
|
|
751
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
752
|
+
throw new Error(`pairwiseEphWindow: from must be a non-negative integer, got ${from}`);
|
|
753
|
+
}
|
|
754
|
+
if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
|
|
755
|
+
throw new Error(
|
|
756
|
+
`pairwiseEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
|
|
757
|
+
);
|
|
758
|
+
}
|
|
562
759
|
for (let i = from; i < from + count; i++) {
|
|
563
760
|
const scalar = bytesToBjjScalar(derivePairwiseEphSk(pairSecret, i));
|
|
564
761
|
const ephPk = fastMulBase(scalar);
|
|
565
762
|
const sharedPoint = fastMulPoint(ivkPoint, scalar);
|
|
566
763
|
entries.push({
|
|
567
764
|
index: i,
|
|
568
|
-
ephPkHex: toHex(bigintTo32Le((0,
|
|
765
|
+
ephPkHex: toHex(bigintTo32Le((0, import_baby_jubjub6.packPoint)(ephPk))),
|
|
569
766
|
sharedSecret: bigintTo32Le(sharedPoint[0])
|
|
570
767
|
});
|
|
571
768
|
}
|
|
@@ -574,26 +771,34 @@ function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
|
|
|
574
771
|
|
|
575
772
|
// src/protocol/keys/PrivacyKeys.ts
|
|
576
773
|
var import_hkdf2 = require("@noble/hashes/hkdf.js");
|
|
577
|
-
var
|
|
578
|
-
var
|
|
774
|
+
var import_sha27 = require("@noble/hashes/sha2.js");
|
|
775
|
+
var import_baby_jubjub7 = require("@zk-kit/baby-jubjub");
|
|
579
776
|
var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
|
|
580
|
-
var
|
|
777
|
+
var SPEND_V3_DOMAIN = new TextEncoder().encode("orbinum-spend-v3");
|
|
778
|
+
var IVK_V3_DOMAIN = new TextEncoder().encode("orbinum-ivk-v3");
|
|
779
|
+
var OVK_V3_DOMAIN = new TextEncoder().encode("orbinum-ovk-v3");
|
|
581
780
|
function deriveViewingPublicKey(ivsk) {
|
|
582
781
|
const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
|
|
583
782
|
const ivkPoint = fastMulBase(ivskScalar);
|
|
584
|
-
const packed = (0,
|
|
783
|
+
const packed = (0, import_baby_jubjub7.packPoint)(ivkPoint);
|
|
585
784
|
return bigintTo32Le(packed);
|
|
586
785
|
}
|
|
587
786
|
|
|
588
787
|
// src/wallet/worker/kernel/types.ts
|
|
589
788
|
var SELF_EPH_WINDOW = 1024;
|
|
590
789
|
var PAIRWISE_EPH_WINDOW = 64;
|
|
790
|
+
var OUTGOING_EPH_WINDOW = 64;
|
|
591
791
|
var EMPTY_BATCH_RESULT = {
|
|
592
792
|
notes: [],
|
|
593
793
|
tagFiltered: 0,
|
|
594
794
|
selfMatched: 0,
|
|
595
795
|
pairwiseMatched: 0,
|
|
596
|
-
maxSelfEphIndex: null
|
|
796
|
+
maxSelfEphIndex: null,
|
|
797
|
+
maxOutgoingEphIndex: null,
|
|
798
|
+
sentNotes: [],
|
|
799
|
+
learnedRecipients: [],
|
|
800
|
+
unmatchedSent: [],
|
|
801
|
+
sealedBookEntries: []
|
|
597
802
|
};
|
|
598
803
|
|
|
599
804
|
// src/wallet/worker/kernel/ephWindow.ts
|
|
@@ -601,23 +806,21 @@ var cachedWindow = null;
|
|
|
601
806
|
function clearKnownEphWindow() {
|
|
602
807
|
cachedWindow = null;
|
|
603
808
|
}
|
|
604
|
-
function bytesKey(bytes) {
|
|
605
|
-
let out = "";
|
|
606
|
-
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
607
|
-
return out;
|
|
608
|
-
}
|
|
609
809
|
function getKnownEphWindow(keys) {
|
|
610
810
|
const selfSize = keys.selfEphWindowSize ?? SELF_EPH_WINDOW;
|
|
611
811
|
const pairSize = keys.pairwiseWindowSize ?? PAIRWISE_EPH_WINDOW;
|
|
812
|
+
const outSize = keys.outgoingEphWindowSize ?? OUTGOING_EPH_WINDOW;
|
|
612
813
|
const counterparties = keys.pairwiseCounterparties ?? [];
|
|
613
|
-
|
|
614
|
-
|
|
814
|
+
const canScanOutgoing = keys.outgoingEph === true && isUsableSecretKey(keys.outgoingViewingKey);
|
|
815
|
+
if (!keys.selfEph && !canScanOutgoing && counterparties.length === 0) return null;
|
|
816
|
+
const cacheKey = `${keys.spendingKey.toString(16)}:${keys.selfEph ? selfSize : 0}:${pairSize}:${canScanOutgoing ? outSize : 0}:` + counterparties.map((c) => toHex(c)).join(",");
|
|
615
817
|
if (cachedWindow?.cacheKey === cacheKey) return cachedWindow.window;
|
|
616
818
|
try {
|
|
617
819
|
const ivkPacked = deriveViewingPublicKey(keys.viewingKey);
|
|
618
820
|
const byEphPk = /* @__PURE__ */ new Map();
|
|
821
|
+
const outgoingByEphPk = /* @__PURE__ */ new Map();
|
|
619
822
|
if (keys.selfEph) {
|
|
620
|
-
for (const e of selfEphWindow(keys.
|
|
823
|
+
for (const e of selfEphWindow(keys.viewingKey, ivkPacked, 0, selfSize)) {
|
|
621
824
|
byEphPk.set(e.ephPkHex.toLowerCase(), {
|
|
622
825
|
sharedSecret: e.sharedSecret,
|
|
623
826
|
index: e.index,
|
|
@@ -638,7 +841,12 @@ function getKnownEphWindow(keys) {
|
|
|
638
841
|
}
|
|
639
842
|
}
|
|
640
843
|
}
|
|
641
|
-
|
|
844
|
+
if (canScanOutgoing) {
|
|
845
|
+
for (const e of outgoingEphWindow(keys.outgoingViewingKey, 0, outSize)) {
|
|
846
|
+
outgoingByEphPk.set(e.ephPkHex.toLowerCase(), e.index);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
cachedWindow = { cacheKey, window: { byEphPk, outgoingByEphPk } };
|
|
642
850
|
return cachedWindow.window;
|
|
643
851
|
} catch {
|
|
644
852
|
return null;
|
|
@@ -647,21 +855,32 @@ function getKnownEphWindow(keys) {
|
|
|
647
855
|
|
|
648
856
|
// src/wallet/worker/kernel/decryptBatch.ts
|
|
649
857
|
function hintEphPkHex(hint) {
|
|
650
|
-
if (hint.ephPkHex) return hint.ephPkHex.toLowerCase();
|
|
651
858
|
const memo = hint.encryptedMemo;
|
|
652
859
|
if (!memo || memo.length < 66) return null;
|
|
653
860
|
return ("0x" + memo.slice(-64)).toLowerCase();
|
|
654
861
|
}
|
|
862
|
+
function sealedBookEntry(note) {
|
|
863
|
+
if (note.sourcePk === void 0 || note.sourcePk === 0n) return null;
|
|
864
|
+
return note.sourcePk;
|
|
865
|
+
}
|
|
655
866
|
function decryptHintBatch(hints, keys) {
|
|
656
867
|
const activation = keys.viewTagActivationLeaf ?? null;
|
|
657
868
|
const knownWindow = getKnownEphWindow(keys);
|
|
869
|
+
const sentNotes = [];
|
|
870
|
+
const learnedRecipients = /* @__PURE__ */ new Set();
|
|
658
871
|
let tagFiltered = 0;
|
|
659
872
|
let selfMatched = 0;
|
|
660
873
|
let pairwiseMatched = 0;
|
|
661
874
|
let maxSelfEphIndex = null;
|
|
875
|
+
let maxOutgoingEphIndex = null;
|
|
876
|
+
const sealedEntries = [];
|
|
877
|
+
const pendingSent = [];
|
|
878
|
+
const seenSent = /* @__PURE__ */ new Set();
|
|
879
|
+
const unmatchedSent = [];
|
|
662
880
|
const notes = hints.map((hint) => {
|
|
663
881
|
try {
|
|
664
|
-
const
|
|
882
|
+
const ephPkHex = hintEphPkHex(hint) ?? "";
|
|
883
|
+
const known = knownWindow?.byEphPk.get(ephPkHex);
|
|
665
884
|
if (known) {
|
|
666
885
|
const result2 = tryDecryptNoteVerbose(
|
|
667
886
|
hint,
|
|
@@ -676,12 +895,26 @@ function decryptHintBatch(hints, keys) {
|
|
|
676
895
|
if (maxSelfEphIndex === null || known.index > maxSelfEphIndex) {
|
|
677
896
|
maxSelfEphIndex = known.index;
|
|
678
897
|
}
|
|
898
|
+
const sealed = sealedBookEntry(result2.note);
|
|
899
|
+
if (sealed !== null) sealedEntries.push(sealed);
|
|
679
900
|
} else {
|
|
680
901
|
pairwiseMatched++;
|
|
681
902
|
}
|
|
682
903
|
return result2.note;
|
|
683
904
|
}
|
|
684
905
|
}
|
|
906
|
+
const outgoingIndex = knownWindow?.outgoingByEphPk.get(ephPkHex);
|
|
907
|
+
if (outgoingIndex !== void 0) {
|
|
908
|
+
if (maxOutgoingEphIndex === null || outgoingIndex > maxOutgoingEphIndex) {
|
|
909
|
+
maxOutgoingEphIndex = outgoingIndex;
|
|
910
|
+
}
|
|
911
|
+
const key = hint.commitmentHex.toLowerCase();
|
|
912
|
+
if (!seenSent.has(key)) {
|
|
913
|
+
seenSent.add(key);
|
|
914
|
+
pendingSent.push({ hint, ephIndex: outgoingIndex });
|
|
915
|
+
}
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
685
918
|
const viewTag = activation !== null && hint.leafIndex >= activation;
|
|
686
919
|
const result = tryDecryptNoteVerbose(
|
|
687
920
|
hint,
|
|
@@ -696,7 +929,44 @@ function decryptHintBatch(hints, keys) {
|
|
|
696
929
|
return null;
|
|
697
930
|
}
|
|
698
931
|
});
|
|
699
|
-
|
|
932
|
+
const carried = keys.recipientCandidates ?? [];
|
|
933
|
+
for (const { hint, ephIndex } of pendingSent) {
|
|
934
|
+
const candidates = [
|
|
935
|
+
...sealedEntries.map(
|
|
936
|
+
(e) => openRecipientBookEntry(e, keys.outgoingViewingKey, hint.commitmentHex)
|
|
937
|
+
),
|
|
938
|
+
...carried
|
|
939
|
+
];
|
|
940
|
+
const sent = recoverSentNote({
|
|
941
|
+
hint,
|
|
942
|
+
outgoingViewingKey: keys.outgoingViewingKey,
|
|
943
|
+
ephIndex,
|
|
944
|
+
recipientCandidates: candidates
|
|
945
|
+
});
|
|
946
|
+
if (!sent) {
|
|
947
|
+
unmatchedSent.push({ hint, ephIndex });
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const { recipientIvk, ...facts } = sent;
|
|
951
|
+
learnedRecipients.add(toHex(recipientIvk));
|
|
952
|
+
sentNotes.push({
|
|
953
|
+
...facts,
|
|
954
|
+
counterpartyIvkHex: toHex(recipientIvk),
|
|
955
|
+
encryptedMemo: hint.encryptedMemo ?? ""
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return {
|
|
959
|
+
notes,
|
|
960
|
+
tagFiltered,
|
|
961
|
+
selfMatched,
|
|
962
|
+
pairwiseMatched,
|
|
963
|
+
maxSelfEphIndex,
|
|
964
|
+
maxOutgoingEphIndex,
|
|
965
|
+
sentNotes,
|
|
966
|
+
learnedRecipients: [...learnedRecipients],
|
|
967
|
+
unmatchedSent,
|
|
968
|
+
sealedBookEntries: sealedEntries.map((e) => e.toString())
|
|
969
|
+
};
|
|
700
970
|
}
|
|
701
971
|
|
|
702
972
|
// src/foundation/errors/abort.ts
|
|
@@ -728,21 +998,52 @@ function createMainThreadPool() {
|
|
|
728
998
|
let selfMatched = 0;
|
|
729
999
|
let pairwiseMatched = 0;
|
|
730
1000
|
let maxSelfEphIndex = null;
|
|
1001
|
+
let maxOutgoingEphIndex = null;
|
|
1002
|
+
const sentNotes = [];
|
|
1003
|
+
const unmatchedSent = [];
|
|
1004
|
+
const sealedBookEntries = /* @__PURE__ */ new Set();
|
|
1005
|
+
const learnedRecipients = new Set(
|
|
1006
|
+
(keys.recipientCandidates ?? []).map((c) => toHex(c))
|
|
1007
|
+
);
|
|
731
1008
|
for (let i = 0; i < hints.length; i += DECRYPT_YIELD_EVERY) {
|
|
732
1009
|
if (i > 0) {
|
|
733
1010
|
await yieldToBrowser();
|
|
734
1011
|
if (signal?.aborted) throw scanAbortError();
|
|
735
1012
|
}
|
|
736
|
-
const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY),
|
|
1013
|
+
const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY), {
|
|
1014
|
+
...keys,
|
|
1015
|
+
recipientCandidates: [...learnedRecipients].map((h) => fromHex(h))
|
|
1016
|
+
});
|
|
737
1017
|
notes.push(...burst.notes);
|
|
738
1018
|
tagFiltered += burst.tagFiltered;
|
|
739
1019
|
selfMatched += burst.selfMatched;
|
|
740
1020
|
pairwiseMatched += burst.pairwiseMatched;
|
|
1021
|
+
sentNotes.push(...burst.sentNotes ?? []);
|
|
1022
|
+
unmatchedSent.push(...burst.unmatchedSent ?? []);
|
|
1023
|
+
for (const e of burst.sealedBookEntries ?? []) sealedBookEntries.add(e);
|
|
1024
|
+
for (const r of burst.learnedRecipients ?? []) learnedRecipients.add(r);
|
|
741
1025
|
if (burst.maxSelfEphIndex !== null) {
|
|
742
1026
|
maxSelfEphIndex = Math.max(maxSelfEphIndex ?? -1, burst.maxSelfEphIndex);
|
|
743
1027
|
}
|
|
1028
|
+
if (burst.maxOutgoingEphIndex != null) {
|
|
1029
|
+
maxOutgoingEphIndex = Math.max(
|
|
1030
|
+
maxOutgoingEphIndex ?? -1,
|
|
1031
|
+
burst.maxOutgoingEphIndex
|
|
1032
|
+
);
|
|
1033
|
+
}
|
|
744
1034
|
}
|
|
745
|
-
return {
|
|
1035
|
+
return {
|
|
1036
|
+
notes,
|
|
1037
|
+
tagFiltered,
|
|
1038
|
+
selfMatched,
|
|
1039
|
+
pairwiseMatched,
|
|
1040
|
+
maxSelfEphIndex,
|
|
1041
|
+
maxOutgoingEphIndex,
|
|
1042
|
+
sentNotes,
|
|
1043
|
+
learnedRecipients: [...learnedRecipients],
|
|
1044
|
+
unmatchedSent,
|
|
1045
|
+
sealedBookEntries: [...sealedBookEntries]
|
|
1046
|
+
};
|
|
746
1047
|
},
|
|
747
1048
|
terminate() {
|
|
748
1049
|
clearKnownEphWindow();
|
|
@@ -774,7 +1075,12 @@ function runOnWorker(worker, payload, signal) {
|
|
|
774
1075
|
tagFiltered: data.tagFiltered ?? 0,
|
|
775
1076
|
selfMatched: data.selfMatched ?? 0,
|
|
776
1077
|
pairwiseMatched: data.pairwiseMatched ?? 0,
|
|
777
|
-
maxSelfEphIndex: data.maxSelfEphIndex ?? null
|
|
1078
|
+
maxSelfEphIndex: data.maxSelfEphIndex ?? null,
|
|
1079
|
+
maxOutgoingEphIndex: data.maxOutgoingEphIndex ?? null,
|
|
1080
|
+
sentNotes: data.sentNotes ?? [],
|
|
1081
|
+
learnedRecipients: data.learnedRecipients ?? [],
|
|
1082
|
+
unmatchedSent: data.unmatchedSent ?? [],
|
|
1083
|
+
sealedBookEntries: data.sealedBookEntries ?? []
|
|
778
1084
|
});
|
|
779
1085
|
};
|
|
780
1086
|
worker.onerror = () => {
|
|
@@ -801,9 +1107,19 @@ function mergeResults(results) {
|
|
|
801
1107
|
tagFiltered: results.reduce((sum, r) => sum + r.tagFiltered, 0),
|
|
802
1108
|
selfMatched: results.reduce((sum, r) => sum + r.selfMatched, 0),
|
|
803
1109
|
pairwiseMatched: results.reduce((sum, r) => sum + r.pairwiseMatched, 0),
|
|
1110
|
+
sentNotes: results.flatMap((r) => r.sentNotes ?? []),
|
|
1111
|
+
// Deduped: slices run in parallel, so the same change note cannot be
|
|
1112
|
+
// seen twice, but two different ones may name the same recipient.
|
|
1113
|
+
learnedRecipients: [...new Set(results.flatMap((r) => r.learnedRecipients ?? []))],
|
|
1114
|
+
unmatchedSent: results.flatMap((r) => r.unmatchedSent ?? []),
|
|
1115
|
+
sealedBookEntries: [...new Set(results.flatMap((r) => r.sealedBookEntries ?? []))],
|
|
804
1116
|
maxSelfEphIndex: results.reduce(
|
|
805
1117
|
(max, r) => r.maxSelfEphIndex === null ? max : Math.max(max ?? -1, r.maxSelfEphIndex),
|
|
806
1118
|
null
|
|
1119
|
+
),
|
|
1120
|
+
maxOutgoingEphIndex: results.reduce(
|
|
1121
|
+
(max, r) => r.maxOutgoingEphIndex == null ? max : Math.max(max ?? -1, r.maxOutgoingEphIndex),
|
|
1122
|
+
null
|
|
807
1123
|
)
|
|
808
1124
|
};
|
|
809
1125
|
}
|
|
@@ -855,6 +1171,7 @@ function createDecryptPool(options) {
|
|
|
855
1171
|
DECRYPT_YIELD_EVERY,
|
|
856
1172
|
EMPTY_BATCH_RESULT,
|
|
857
1173
|
MAX_WORKERS,
|
|
1174
|
+
OUTGOING_EPH_WINDOW,
|
|
858
1175
|
PAIRWISE_EPH_WINDOW,
|
|
859
1176
|
SELF_EPH_WINDOW,
|
|
860
1177
|
WORKER_CRASHED,
|