@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
|
@@ -4,7 +4,7 @@ var CURRENT_CIRCUIT_VERSION = 1;
|
|
|
4
4
|
// src/protocol/memo/EncryptedMemo.ts
|
|
5
5
|
import { chacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
6
6
|
import { randomBytes } from "@noble/ciphers/utils.js";
|
|
7
|
-
import { packPoint
|
|
7
|
+
import { packPoint } from "@zk-kit/baby-jubjub";
|
|
8
8
|
|
|
9
9
|
// src/foundation/crypto/bjj-fast.ts
|
|
10
10
|
import { edwards } from "@noble/curves/abstract/edwards.js";
|
|
@@ -32,6 +32,84 @@ function fastMulPoint(point, scalar) {
|
|
|
32
32
|
return [x, y];
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// src/foundation/crypto/bjj.ts
|
|
36
|
+
import { mulPointEscalar, unpackPoint } from "@zk-kit/baby-jubjub";
|
|
37
|
+
|
|
38
|
+
// src/foundation/crypto/constants.ts
|
|
39
|
+
var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
40
|
+
var BABYJUB_SUBORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
|
|
41
|
+
|
|
42
|
+
// src/foundation/crypto/bjj.ts
|
|
43
|
+
var BJJ_A = 168700n;
|
|
44
|
+
var BJJ_D = 168696n;
|
|
45
|
+
var BJJ_COFACTOR = 8n;
|
|
46
|
+
function _modpow(base, exp, mod) {
|
|
47
|
+
let result = 1n;
|
|
48
|
+
base = base % mod;
|
|
49
|
+
while (exp > 0n) {
|
|
50
|
+
if (exp & 1n) result = result * base % mod;
|
|
51
|
+
exp >>= 1n;
|
|
52
|
+
base = base * base % mod;
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function _sqrtModP(y2) {
|
|
57
|
+
if (y2 === 0n) return 0n;
|
|
58
|
+
if (_modpow(y2, (BN254_R - 1n) / 2n, BN254_R) !== 1n) return null;
|
|
59
|
+
let s = 0n;
|
|
60
|
+
let q = BN254_R - 1n;
|
|
61
|
+
while ((q & 1n) === 0n) {
|
|
62
|
+
q >>= 1n;
|
|
63
|
+
s++;
|
|
64
|
+
}
|
|
65
|
+
if (s === 1n) return _modpow(y2, (BN254_R + 1n) / 4n, BN254_R);
|
|
66
|
+
let z = 2n;
|
|
67
|
+
while (_modpow(z, (BN254_R - 1n) / 2n, BN254_R) === 1n) z++;
|
|
68
|
+
let m = s;
|
|
69
|
+
let c = _modpow(z, q, BN254_R);
|
|
70
|
+
let t = _modpow(y2, q, BN254_R);
|
|
71
|
+
let r = _modpow(y2, (q + 1n) / 2n, BN254_R);
|
|
72
|
+
for (; ; ) {
|
|
73
|
+
if (t === 1n) return r;
|
|
74
|
+
let i = 1n;
|
|
75
|
+
let tmp = t * t % BN254_R;
|
|
76
|
+
while (tmp !== 1n) {
|
|
77
|
+
tmp = tmp * tmp % BN254_R;
|
|
78
|
+
i++;
|
|
79
|
+
}
|
|
80
|
+
const b = _modpow(c, 1n << m - i - 1n, BN254_R);
|
|
81
|
+
m = i;
|
|
82
|
+
c = b * b % BN254_R;
|
|
83
|
+
t = t * c % BN254_R;
|
|
84
|
+
r = r * b % BN254_R;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function recoverOwnerPkPoint(ax) {
|
|
88
|
+
const x2 = ax * ax % BN254_R;
|
|
89
|
+
const num = ((1n - BJJ_A * x2) % BN254_R + BN254_R) % BN254_R;
|
|
90
|
+
const den = ((1n - BJJ_D * x2) % BN254_R + BN254_R) % BN254_R;
|
|
91
|
+
if (den === 0n) return null;
|
|
92
|
+
const denInv = _modpow(den, BN254_R - 2n, BN254_R);
|
|
93
|
+
const y2 = num * denInv % BN254_R;
|
|
94
|
+
const y = _sqrtModP(y2);
|
|
95
|
+
if (y === null) return null;
|
|
96
|
+
const yAlt = BN254_R - y;
|
|
97
|
+
try {
|
|
98
|
+
const check = mulPointEscalar([ax, y], BABYJUB_SUBORDER);
|
|
99
|
+
return check[0] === 0n && check[1] === 1n ? [ax, y] : [ax, yAlt];
|
|
100
|
+
} catch {
|
|
101
|
+
return [ax, yAlt];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function unpackUsableViewingKey(packed) {
|
|
105
|
+
const point = unpackPoint(packed);
|
|
106
|
+
if (!point) return null;
|
|
107
|
+
if (point[0] === 0n && point[1] === 1n) return null;
|
|
108
|
+
const cleared = mulPointEscalar(point, BJJ_COFACTOR);
|
|
109
|
+
if (cleared[0] === 0n && cleared[1] === 1n) return null;
|
|
110
|
+
return point;
|
|
111
|
+
}
|
|
112
|
+
|
|
35
113
|
// src/foundation/encoding/hex.ts
|
|
36
114
|
function isHexOfLength(value, byteLen) {
|
|
37
115
|
return typeof value === "string" && new RegExp(`^0x[0-9a-fA-F]{${byteLen * 2}}$`).test(value);
|
|
@@ -57,12 +135,23 @@ function ensureHexPrefix(hex) {
|
|
|
57
135
|
return hex.startsWith("0x") ? hex : `0x${hex}`;
|
|
58
136
|
}
|
|
59
137
|
function hexToNumber(hex) {
|
|
60
|
-
|
|
138
|
+
const clean = /^0x/i.test(hex) ? hex.slice(2) : hex;
|
|
139
|
+
if (!/^[0-9a-fA-F]+$/.test(clean)) {
|
|
140
|
+
throw new Error(`Invalid hex quantity: "${hex}"`);
|
|
141
|
+
}
|
|
142
|
+
const value = Number(BigInt("0x" + clean));
|
|
143
|
+
if (!Number.isSafeInteger(value)) {
|
|
144
|
+
throw new Error(`Hex quantity exceeds safe integer range: "${hex}"`);
|
|
145
|
+
}
|
|
146
|
+
return value;
|
|
61
147
|
}
|
|
62
148
|
function hexToBigint(hex) {
|
|
63
149
|
return BigInt(hex);
|
|
64
150
|
}
|
|
65
151
|
function scalarToHex(value) {
|
|
152
|
+
if (value < 0n || value >> 256n !== 0n) {
|
|
153
|
+
throw new Error(`scalarToHex: value must fit in 32 unsigned bytes, got ${value}`);
|
|
154
|
+
}
|
|
66
155
|
return "0x" + value.toString(16).padStart(64, "0");
|
|
67
156
|
}
|
|
68
157
|
|
|
@@ -122,16 +211,21 @@ function leHexToBigint(hex) {
|
|
|
122
211
|
return bytesToBigintLE(fromHex(hex));
|
|
123
212
|
}
|
|
124
213
|
|
|
125
|
-
// src/foundation/crypto/constants.ts
|
|
126
|
-
var BN254_R = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
127
|
-
var BABYJUB_SUBORDER = 2736030358979909402780800718157159386076813972158567259200215660948447373041n;
|
|
128
|
-
|
|
129
214
|
// src/protocol/memo/plaintext.ts
|
|
130
215
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
131
216
|
var KEY_DOMAIN = new TextEncoder().encode("orbinum-note-encryption-v1");
|
|
132
217
|
var VIEW_TAG_DOMAIN = new TextEncoder().encode("orbinum-view-tag-v1");
|
|
133
218
|
var MEMO_PLAINTEXT_SIZE = 120;
|
|
134
219
|
function serializeMemo(value, ownerPk, blinding, assetId, sourcePk, circuitVersion) {
|
|
220
|
+
if (value < 0n || value >> 128n !== 0n) {
|
|
221
|
+
throw new Error(`serializeMemo: value must fit in 128 unsigned bits, got ${value}`);
|
|
222
|
+
}
|
|
223
|
+
if (!Number.isInteger(assetId) || assetId < 0 || assetId > 4294967295) {
|
|
224
|
+
throw new Error(`serializeMemo: assetId must be a u32, got ${assetId}`);
|
|
225
|
+
}
|
|
226
|
+
if (!Number.isInteger(circuitVersion) || circuitVersion < 0 || circuitVersion > 4294967295) {
|
|
227
|
+
throw new Error(`serializeMemo: circuitVersion must be a u32, got ${circuitVersion}`);
|
|
228
|
+
}
|
|
135
229
|
const buf = new Uint8Array(MEMO_PLAINTEXT_SIZE);
|
|
136
230
|
const view = new DataView(buf.buffer);
|
|
137
231
|
view.setBigUint64(0, value & 0xffffffffffffffffn, true);
|
|
@@ -163,6 +257,9 @@ var CIPHERTEXT_SIZE = 136;
|
|
|
163
257
|
var EPH_PK_SIZE = 32;
|
|
164
258
|
var ENCRYPTED_MEMO_SIZE = NONCE_SIZE + CIPHERTEXT_SIZE + EPH_PK_SIZE;
|
|
165
259
|
function bytesToBjjScalar(bytes) {
|
|
260
|
+
if (bytes.length !== 32) {
|
|
261
|
+
throw new Error(`bytesToBjjScalar: expected 32 bytes, got ${bytes.length}`);
|
|
262
|
+
}
|
|
166
263
|
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
167
264
|
return BigInt("0x" + hex) % BABYJUB_SUBORDER || 1n;
|
|
168
265
|
}
|
|
@@ -186,7 +283,7 @@ function parsePlaintext(nonce, ciphertextWithMac, encKey) {
|
|
|
186
283
|
}
|
|
187
284
|
var EncryptedMemo = {
|
|
188
285
|
/**
|
|
189
|
-
* Build and encrypt a memo for a note using ECDH (
|
|
286
|
+
* Build and encrypt a memo for a note using ECDH (180 bytes).
|
|
190
287
|
*
|
|
191
288
|
* @param value Note value in planck.
|
|
192
289
|
* @param ownerPk 32-byte owner public key (LE).
|
|
@@ -226,7 +323,7 @@ var EncryptedMemo = {
|
|
|
226
323
|
const ephPkPoint = fastMulBase(ephSkScalar);
|
|
227
324
|
ephPkPackedBytes = bigintTo32Le(packPoint(ephPkPoint));
|
|
228
325
|
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
229
|
-
const ivkPoint =
|
|
326
|
+
const ivkPoint = unpackUsableViewingKey(ivkPackedBigint);
|
|
230
327
|
if (!ivkPoint)
|
|
231
328
|
throw new Error("EncryptedMemo.encrypt: invalid recipient viewing public key");
|
|
232
329
|
const sharedPoint = fastMulPoint(ivkPoint, ephSkScalar);
|
|
@@ -316,7 +413,7 @@ var EncryptedMemo = {
|
|
|
316
413
|
if (ephPkPackedBigint === 0n) {
|
|
317
414
|
return new Uint8Array(32);
|
|
318
415
|
}
|
|
319
|
-
const ephPkPoint =
|
|
416
|
+
const ephPkPoint = unpackUsableViewingKey(ephPkPackedBigint);
|
|
320
417
|
if (!ephPkPoint) return null;
|
|
321
418
|
const ivskScalar = bytesToBjjScalar(viewingSecretKey);
|
|
322
419
|
const sharedPoint = fastMulPoint(ephPkPoint, ivskScalar);
|
|
@@ -326,9 +423,9 @@ var EncryptedMemo = {
|
|
|
326
423
|
* Cheap view-tag check: does memo nonce[0] match the tag derived from
|
|
327
424
|
* `sharedSecret`? One SHA256 + one byte compare — no AEAD work.
|
|
328
425
|
*
|
|
329
|
-
* Only meaningful for memos
|
|
330
|
-
*
|
|
331
|
-
*
|
|
426
|
+
* Only meaningful for memos that carry a tag — at or after
|
|
427
|
+
* `ScanKeys.viewTagActivationLeaf`. An older memo has a random byte there
|
|
428
|
+
* and would false-negative 255/256 of the time.
|
|
332
429
|
*/
|
|
333
430
|
checkViewTag(memoBytes, sharedSecret) {
|
|
334
431
|
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return false;
|
|
@@ -354,80 +451,19 @@ var EncryptedMemo = {
|
|
|
354
451
|
}
|
|
355
452
|
};
|
|
356
453
|
|
|
357
|
-
// src/protocol/memo/OutgoingBlob.ts
|
|
358
|
-
import { chacha20poly1305 as chacha20poly13052 } from "@noble/ciphers/chacha.js";
|
|
359
|
-
import { randomBytes as randomBytes2 } from "@noble/ciphers/utils.js";
|
|
360
|
-
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
361
|
-
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
362
|
-
var OCK_DOMAIN = new TextEncoder().encode("orbinum-outgoing-cipher-v1");
|
|
363
|
-
var NONCE_PREFIX = new TextEncoder().encode("OVK1");
|
|
364
|
-
var NONCE_SUFFIX_SIZE = 8;
|
|
365
|
-
var SHARED_SECRET_SIZE = 32;
|
|
366
|
-
var MAC_SIZE = 16;
|
|
367
|
-
var OVK_BLOB_SIZE = NONCE_SUFFIX_SIZE + SHARED_SECRET_SIZE + MAC_SIZE;
|
|
368
|
-
function deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes) {
|
|
369
|
-
if (commitmentBytes.length !== 32) {
|
|
370
|
-
throw new Error(
|
|
371
|
-
`OutgoingBlob: commitmentBytes must be 32 bytes, got ${commitmentBytes.length}`
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
if (ephPkBytes.length !== 32) {
|
|
375
|
-
throw new Error(`OutgoingBlob: ephPkBytes must be 32 bytes, got ${ephPkBytes.length}`);
|
|
376
|
-
}
|
|
377
|
-
const salt = new Uint8Array(64);
|
|
378
|
-
salt.set(commitmentBytes, 0);
|
|
379
|
-
salt.set(ephPkBytes, 32);
|
|
380
|
-
return hkdf(sha2562, ovk, salt, OCK_DOMAIN, 32);
|
|
381
|
-
}
|
|
382
|
-
function buildNonce(suffix) {
|
|
383
|
-
const nonce = new Uint8Array(12);
|
|
384
|
-
nonce.set(NONCE_PREFIX, 0);
|
|
385
|
-
nonce.set(suffix, 4);
|
|
386
|
-
return nonce;
|
|
387
|
-
}
|
|
388
|
-
function sealOutgoingBlob(ovk, sharedSecret, commitmentBytes, ephPkBytes) {
|
|
389
|
-
if (sharedSecret.length !== SHARED_SECRET_SIZE) {
|
|
390
|
-
throw new Error(`OutgoingBlob: sharedSecret must be 32 bytes, got ${sharedSecret.length}`);
|
|
391
|
-
}
|
|
392
|
-
const ock = deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes);
|
|
393
|
-
const suffix = randomBytes2(NONCE_SUFFIX_SIZE);
|
|
394
|
-
const cipher = chacha20poly13052(ock, buildNonce(suffix));
|
|
395
|
-
const sealed = cipher.encrypt(sharedSecret);
|
|
396
|
-
const blob = new Uint8Array(OVK_BLOB_SIZE);
|
|
397
|
-
blob.set(suffix, 0);
|
|
398
|
-
blob.set(sealed, NONCE_SUFFIX_SIZE);
|
|
399
|
-
return blob;
|
|
400
|
-
}
|
|
401
|
-
function openOutgoingBlob(ovk, blob, commitmentBytes, ephPkBytes) {
|
|
402
|
-
if (blob.length !== OVK_BLOB_SIZE) return null;
|
|
403
|
-
if (commitmentBytes.length !== 32 || ephPkBytes.length !== 32) return null;
|
|
404
|
-
try {
|
|
405
|
-
const ock = deriveOutgoingCipherKey(ovk, commitmentBytes, ephPkBytes);
|
|
406
|
-
const suffix = blob.subarray(0, NONCE_SUFFIX_SIZE);
|
|
407
|
-
const sealed = blob.subarray(NONCE_SUFFIX_SIZE);
|
|
408
|
-
const cipher = chacha20poly13052(ock, buildNonce(suffix));
|
|
409
|
-
return cipher.decrypt(sealed);
|
|
410
|
-
} catch {
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
function randomOutgoingBlob() {
|
|
415
|
-
return randomBytes2(OVK_BLOB_SIZE);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
454
|
// src/foundation/crypto/stealth.ts
|
|
419
|
-
import { sha256 as
|
|
420
|
-
import { hkdf
|
|
421
|
-
import { mulPointEscalar, Base8, addPoint } from "@zk-kit/baby-jubjub";
|
|
455
|
+
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
456
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
457
|
+
import { mulPointEscalar as mulPointEscalar2, Base8, addPoint } from "@zk-kit/baby-jubjub";
|
|
422
458
|
var STEALTH_INFO = new TextEncoder().encode("orbinum-stealth-v1");
|
|
423
459
|
function deriveStealthScalar(sharedSecret, ownerPkBigint) {
|
|
424
460
|
const salt = bigintTo32Le(ownerPkBigint);
|
|
425
|
-
const stealthBytes =
|
|
461
|
+
const stealthBytes = hkdf(sha2562, sharedSecret, salt, STEALTH_INFO, 32);
|
|
426
462
|
return bytesToBigintLE(stealthBytes) % BABYJUB_SUBORDER || 1n;
|
|
427
463
|
}
|
|
428
464
|
function deriveStealthOwnerPk(sharedSecret, ownerPkBigint, ownerPkPoint) {
|
|
429
465
|
const stealthScalar = deriveStealthScalar(sharedSecret, ownerPkBigint);
|
|
430
|
-
const stealthPt = addPoint(
|
|
466
|
+
const stealthPt = addPoint(mulPointEscalar2(Base8, stealthScalar), ownerPkPoint);
|
|
431
467
|
return stealthPt[0];
|
|
432
468
|
}
|
|
433
469
|
function deriveStealthSk(sharedSecret, ownerPkBigint, spendingKey) {
|
|
@@ -435,73 +471,20 @@ function deriveStealthSk(sharedSecret, ownerPkBigint, spendingKey) {
|
|
|
435
471
|
return (stealthScalar + spendingKey) % BABYJUB_SUBORDER || 1n;
|
|
436
472
|
}
|
|
437
473
|
|
|
438
|
-
// src/
|
|
439
|
-
import { mulPointEscalar as
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
}
|
|
450
|
-
return result;
|
|
451
|
-
}
|
|
452
|
-
function _sqrtModP(y2) {
|
|
453
|
-
if (y2 === 0n) return 0n;
|
|
454
|
-
if (_modpow(y2, (BN254_R - 1n) / 2n, BN254_R) !== 1n) return null;
|
|
455
|
-
let s = 0n;
|
|
456
|
-
let q = BN254_R - 1n;
|
|
457
|
-
while ((q & 1n) === 0n) {
|
|
458
|
-
q >>= 1n;
|
|
459
|
-
s++;
|
|
460
|
-
}
|
|
461
|
-
if (s === 1n) return _modpow(y2, (BN254_R + 1n) / 4n, BN254_R);
|
|
462
|
-
let z = 2n;
|
|
463
|
-
while (_modpow(z, (BN254_R - 1n) / 2n, BN254_R) === 1n) z++;
|
|
464
|
-
let m = s;
|
|
465
|
-
let c = _modpow(z, q, BN254_R);
|
|
466
|
-
let t = _modpow(y2, q, BN254_R);
|
|
467
|
-
let r = _modpow(y2, (q + 1n) / 2n, BN254_R);
|
|
468
|
-
for (; ; ) {
|
|
469
|
-
if (t === 1n) return r;
|
|
470
|
-
let i = 1n;
|
|
471
|
-
let tmp = t * t % BN254_R;
|
|
472
|
-
while (tmp !== 1n) {
|
|
473
|
-
tmp = tmp * tmp % BN254_R;
|
|
474
|
-
i++;
|
|
475
|
-
}
|
|
476
|
-
const b = _modpow(c, 1n << m - i - 1n, BN254_R);
|
|
477
|
-
m = i;
|
|
478
|
-
c = b * b % BN254_R;
|
|
479
|
-
t = t * c % BN254_R;
|
|
480
|
-
r = r * b % BN254_R;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
function recoverOwnerPkPoint(ax) {
|
|
484
|
-
const x2 = ax * ax % BN254_R;
|
|
485
|
-
const num = ((1n - BJJ_A * x2) % BN254_R + BN254_R) % BN254_R;
|
|
486
|
-
const den = ((1n - BJJ_D * x2) % BN254_R + BN254_R) % BN254_R;
|
|
487
|
-
if (den === 0n) return null;
|
|
488
|
-
const denInv = _modpow(den, BN254_R - 2n, BN254_R);
|
|
489
|
-
const y2 = num * denInv % BN254_R;
|
|
490
|
-
const y = _sqrtModP(y2);
|
|
491
|
-
if (y === null) return null;
|
|
492
|
-
const yAlt = BN254_R - y;
|
|
493
|
-
try {
|
|
494
|
-
const check = mulPointEscalar2([ax, y], BABYJUB_SUBORDER);
|
|
495
|
-
return check[0] === 0n && check[1] === 1n ? [ax, y] : [ax, yAlt];
|
|
496
|
-
} catch {
|
|
497
|
-
return [ax, yAlt];
|
|
498
|
-
}
|
|
474
|
+
// src/protocol/note/NoteBuilder.ts
|
|
475
|
+
import { mulPointEscalar as mulPointEscalar3 } from "@zk-kit/baby-jubjub";
|
|
476
|
+
import { randomBytes as randomBytes2 } from "@noble/ciphers/utils.js";
|
|
477
|
+
import { poseidon2, poseidon4 } from "poseidon-lite";
|
|
478
|
+
|
|
479
|
+
// src/foundation/crypto/blinding.ts
|
|
480
|
+
function randomBlinding() {
|
|
481
|
+
const buf = new Uint8Array(32);
|
|
482
|
+
crypto.getRandomValues(buf);
|
|
483
|
+
const reduced = bytesToBigintLE(buf) % BN254_R;
|
|
484
|
+
return reduced === 0n ? 1n : reduced;
|
|
499
485
|
}
|
|
500
486
|
|
|
501
487
|
// src/protocol/note/NoteBuilder.ts
|
|
502
|
-
import { mulPointEscalar as mulPointEscalar3, unpackPoint as unpackPoint2 } from "@zk-kit/baby-jubjub";
|
|
503
|
-
import { randomBytes as randomBytes3 } from "@noble/ciphers/utils.js";
|
|
504
|
-
import { poseidon2, poseidon4 } from "poseidon-lite";
|
|
505
488
|
var NoteBuilder = class {
|
|
506
489
|
/**
|
|
507
490
|
* Build a ZkNote from the given inputs.
|
|
@@ -509,7 +492,7 @@ var NoteBuilder = class {
|
|
|
509
492
|
* @param input.value Amount in planck (required).
|
|
510
493
|
* @param input.assetId Asset ID — default 0n (native ORB-Privacy).
|
|
511
494
|
* @param input.ownerPk Sender's or recipient's global BabyJubJub Ax — default 0n.
|
|
512
|
-
* @param input.blinding Random scalar — defaults to
|
|
495
|
+
* @param input.blinding Random scalar — defaults to a CSPRNG draw.
|
|
513
496
|
* @param input.spendingKey Secret key for nullifier — default 0n.
|
|
514
497
|
* @param input.viewingPublicKey Recipient's 32-byte LE packed BJJ ivk. Triggers memo encryption.
|
|
515
498
|
* @param input.recipientOwnerPk Recipient's global ownerPk. Required with viewingPublicKey
|
|
@@ -520,7 +503,7 @@ var NoteBuilder = class {
|
|
|
520
503
|
const value = input.value;
|
|
521
504
|
const assetId = input.assetId ?? 0n;
|
|
522
505
|
const ownerPk = input.ownerPk ?? 0n;
|
|
523
|
-
const blinding = input.blinding ??
|
|
506
|
+
const blinding = input.blinding ?? randomBlinding();
|
|
524
507
|
const spendingKey = input.spendingKey ?? 0n;
|
|
525
508
|
const sourcePk = input.sourcePk ?? 0n;
|
|
526
509
|
const circuitVersion = input.circuitVersion ?? CURRENT_CIRCUIT_VERSION;
|
|
@@ -529,9 +512,9 @@ var NoteBuilder = class {
|
|
|
529
512
|
if (useStealth) {
|
|
530
513
|
const recipientOwnerPk = input.recipientOwnerPk;
|
|
531
514
|
const recipientIvkPacked = input.viewingPublicKey;
|
|
532
|
-
const ephSk = input.ephSkOverride ??
|
|
515
|
+
const ephSk = input.ephSkOverride ?? randomBytes2(32);
|
|
533
516
|
const ivkPackedBigint = bytesToBigintLE(recipientIvkPacked);
|
|
534
|
-
const ivkPoint =
|
|
517
|
+
const ivkPoint = unpackUsableViewingKey(ivkPackedBigint);
|
|
535
518
|
if (!ivkPoint)
|
|
536
519
|
throw new Error("NoteBuilder.build: invalid recipient viewing public key");
|
|
537
520
|
const ephSkScalar = BigInt(toHex(ephSk)) % BABYJUB_SUBORDER || 1n;
|
|
@@ -570,21 +553,6 @@ var NoteBuilder = class {
|
|
|
570
553
|
throw new Error(
|
|
571
554
|
`NoteBuilder.build: invariant violated \u2014 memo must be ${ENCRYPTED_MEMO_SIZE} bytes, got ${memo.length}`
|
|
572
555
|
);
|
|
573
|
-
let ovkBlob;
|
|
574
|
-
if (input.outgoingViewingKey !== void 0) {
|
|
575
|
-
const ephPkBytes = Uint8Array.from(memo.slice(148, 180));
|
|
576
|
-
const sealed = sealOutgoingBlob(
|
|
577
|
-
input.outgoingViewingKey,
|
|
578
|
-
sharedSecret,
|
|
579
|
-
stealthCommitmentBytes,
|
|
580
|
-
ephPkBytes
|
|
581
|
-
);
|
|
582
|
-
if (sealed.length !== OVK_BLOB_SIZE)
|
|
583
|
-
throw new Error(
|
|
584
|
-
`NoteBuilder.build: invariant violated \u2014 ovkBlob must be ${OVK_BLOB_SIZE} bytes, got ${sealed.length}`
|
|
585
|
-
);
|
|
586
|
-
ovkBlob = Array.from(sealed);
|
|
587
|
-
}
|
|
588
556
|
return {
|
|
589
557
|
value,
|
|
590
558
|
assetId,
|
|
@@ -599,8 +567,7 @@ var NoteBuilder = class {
|
|
|
599
567
|
commitmentHex: toHex(commitmentBytes2),
|
|
600
568
|
nullifierHex: toHex(nullifierBytes2),
|
|
601
569
|
memo,
|
|
602
|
-
sourcePk
|
|
603
|
-
...ovkBlob ? { ovkBlob } : {}
|
|
570
|
+
sourcePk
|
|
604
571
|
};
|
|
605
572
|
}
|
|
606
573
|
const commitment = poseidon4([value, assetId, ownerPk, blinding]);
|
|
@@ -650,7 +617,7 @@ var NoteBuilder = class {
|
|
|
650
617
|
* @param note The ZkNote whose fields populate the plaintext.
|
|
651
618
|
* @param recipientIvkPacked 32-byte LE packed BJJ viewing public key of the recipient.
|
|
652
619
|
* Pass `new Uint8Array(32)` (default) for a public/dummy memo.
|
|
653
|
-
* @param sourcePk
|
|
620
|
+
* @param sourcePk 32-byte counterparty BabyJubJub Ax.
|
|
654
621
|
* Pass `new Uint8Array(32)` (default) for no counterparty.
|
|
655
622
|
*/
|
|
656
623
|
static buildMemo(note, recipientIvkPacked, sourcePk) {
|
|
@@ -730,18 +697,6 @@ function buildDummyTransferInput(assetId) {
|
|
|
730
697
|
}
|
|
731
698
|
|
|
732
699
|
// src/protocol/note/NoteDecryptor.ts
|
|
733
|
-
function decryptAndVerifyPlaintext(memoBytes, commitmentBytes, sharedSecret, effectiveOwnerPk) {
|
|
734
|
-
const plaintext = EncryptedMemo.decryptWithSharedSecret(
|
|
735
|
-
memoBytes,
|
|
736
|
-
commitmentBytes,
|
|
737
|
-
sharedSecret
|
|
738
|
-
);
|
|
739
|
-
if (!plaintext) return null;
|
|
740
|
-
const ownerPk = effectiveOwnerPk ?? plaintext.ownerPk;
|
|
741
|
-
const recomputed = poseidon42([plaintext.value, plaintext.assetId, ownerPk, plaintext.blinding]);
|
|
742
|
-
if (recomputed !== bytesToBigintLE(commitmentBytes)) return null;
|
|
743
|
-
return plaintext;
|
|
744
|
-
}
|
|
745
700
|
function computeNullifier(commitment, spendingKey) {
|
|
746
701
|
return poseidon22([commitment, spendingKey]);
|
|
747
702
|
}
|
|
@@ -824,40 +779,6 @@ function tryDecryptNoteVerbose(commitment, viewingSecretKey, spendingKey, ownOwn
|
|
|
824
779
|
}
|
|
825
780
|
};
|
|
826
781
|
}
|
|
827
|
-
function tryRecoverOutgoing(hint, ovk, opts) {
|
|
828
|
-
if (!hint.ovkBlob || !hint.encryptedMemo) return null;
|
|
829
|
-
let commitmentBytes;
|
|
830
|
-
let memoBytes;
|
|
831
|
-
let blobBytes;
|
|
832
|
-
try {
|
|
833
|
-
commitmentBytes = fromHex(hint.commitmentHex);
|
|
834
|
-
memoBytes = fromHex(hint.encryptedMemo);
|
|
835
|
-
blobBytes = fromHex(hint.ovkBlob);
|
|
836
|
-
} catch {
|
|
837
|
-
return null;
|
|
838
|
-
}
|
|
839
|
-
if (memoBytes.length !== ENCRYPTED_MEMO_SIZE) return null;
|
|
840
|
-
if (blobBytes.length !== OVK_BLOB_SIZE) return null;
|
|
841
|
-
const ephPkBytes = memoBytes.subarray(148, 180);
|
|
842
|
-
const sharedSecret = openOutgoingBlob(ovk, blobBytes, commitmentBytes, ephPkBytes);
|
|
843
|
-
if (!sharedSecret) return null;
|
|
844
|
-
const activation = opts?.viewTagActivationLeaf;
|
|
845
|
-
if (activation !== void 0 && isValidLeafIndex(hint.leafIndex) && hint.leafIndex >= activation && !EncryptedMemo.checkViewTag(memoBytes, sharedSecret)) {
|
|
846
|
-
return null;
|
|
847
|
-
}
|
|
848
|
-
const plaintext = decryptAndVerifyPlaintext(memoBytes, commitmentBytes, sharedSecret, null);
|
|
849
|
-
if (!plaintext) return null;
|
|
850
|
-
return {
|
|
851
|
-
commitmentHex: hint.commitmentHex,
|
|
852
|
-
...isValidLeafIndex(hint.leafIndex) ? { leafIndex: hint.leafIndex } : {},
|
|
853
|
-
value: plaintext.value,
|
|
854
|
-
assetId: plaintext.assetId,
|
|
855
|
-
recipientStealthPk: plaintext.ownerPk,
|
|
856
|
-
blinding: plaintext.blinding,
|
|
857
|
-
sourcePk: plaintext.sourcePk,
|
|
858
|
-
circuitVersion: plaintext.circuitVersion
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
782
|
function collectOutgoingFacts(hint) {
|
|
862
783
|
if (!isHexOfLength(hint.commitmentHex, 32)) return null;
|
|
863
784
|
if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
|
|
@@ -868,6 +789,160 @@ function collectOutgoingFacts(hint) {
|
|
|
868
789
|
};
|
|
869
790
|
}
|
|
870
791
|
|
|
792
|
+
// src/protocol/eph/outgoingEph.ts
|
|
793
|
+
import { sha256 as sha2563 } from "@noble/hashes/sha2.js";
|
|
794
|
+
|
|
795
|
+
// src/protocol/eph/windowBounds.ts
|
|
796
|
+
var MAX_EPH_WINDOW = 1 << 20;
|
|
797
|
+
|
|
798
|
+
// src/foundation/crypto/keyGuards.ts
|
|
799
|
+
var SECRET_KEY_SIZE = 32;
|
|
800
|
+
function isUsableSecretKey(key) {
|
|
801
|
+
return key !== void 0 && key !== null && key.length === SECRET_KEY_SIZE && !key.every((b) => b === 0);
|
|
802
|
+
}
|
|
803
|
+
function assertSecretKeyBytes(key, label) {
|
|
804
|
+
if (key.length !== SECRET_KEY_SIZE) {
|
|
805
|
+
throw new Error(`${label} must be ${SECRET_KEY_SIZE} bytes, got ${key.length}`);
|
|
806
|
+
}
|
|
807
|
+
if (key.every((b) => b === 0)) {
|
|
808
|
+
throw new Error(`${label} is all zeros \u2014 that is not key material`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/protocol/eph/outgoingEph.ts
|
|
813
|
+
import { packPoint as packPoint2, unpackPoint as unpackPoint2 } from "@zk-kit/baby-jubjub";
|
|
814
|
+
var OUTGOING_EPH_DOMAIN = new TextEncoder().encode("orbinum-outgoing-eph-v3");
|
|
815
|
+
function deriveOutgoingEphSk(outgoingViewingKey, index) {
|
|
816
|
+
if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
|
|
817
|
+
throw new Error(`deriveOutgoingEphSk: index must be a u32, got ${index}`);
|
|
818
|
+
}
|
|
819
|
+
assertSecretKeyBytes(outgoingViewingKey, "deriveOutgoingEphSk: outgoingViewingKey");
|
|
820
|
+
const h = sha2563.create();
|
|
821
|
+
h.update(OUTGOING_EPH_DOMAIN);
|
|
822
|
+
h.update(outgoingViewingKey);
|
|
823
|
+
const idx = new Uint8Array(4);
|
|
824
|
+
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
825
|
+
h.update(idx);
|
|
826
|
+
return h.digest();
|
|
827
|
+
}
|
|
828
|
+
function deriveOutgoingEphPk(outgoingViewingKey, index) {
|
|
829
|
+
const scalar = bytesToBjjScalar(deriveOutgoingEphSk(outgoingViewingKey, index));
|
|
830
|
+
return toHex(bigintTo32Le(packPoint2(fastMulBase(scalar)))).toLowerCase();
|
|
831
|
+
}
|
|
832
|
+
function outgoingEphWindow(outgoingViewingKey, from, count) {
|
|
833
|
+
const entries = [];
|
|
834
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
835
|
+
throw new Error(`outgoingEphWindow: from must be a non-negative integer, got ${from}`);
|
|
836
|
+
}
|
|
837
|
+
if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
|
|
838
|
+
throw new Error(
|
|
839
|
+
`outgoingEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
for (let i = from; i < from + count; i++) {
|
|
843
|
+
entries.push({ index: i, ephPkHex: deriveOutgoingEphPk(outgoingViewingKey, i) });
|
|
844
|
+
}
|
|
845
|
+
return entries;
|
|
846
|
+
}
|
|
847
|
+
function deriveOutgoingSharedSecret(outgoingViewingKey, index, recipientIvkPacked) {
|
|
848
|
+
const ivkPoint = unpackPoint2(bytesToBigintLE(recipientIvkPacked));
|
|
849
|
+
if (!ivkPoint) throw new Error("deriveOutgoingSharedSecret: invalid viewing public key");
|
|
850
|
+
const scalar = bytesToBjjScalar(deriveOutgoingEphSk(outgoingViewingKey, index));
|
|
851
|
+
return bigintTo32Le(fastMulPoint(ivkPoint, scalar)[0]);
|
|
852
|
+
}
|
|
853
|
+
function reconstructOutgoingIndex(outgoingViewingKey, publishedEphPks, window) {
|
|
854
|
+
const published = /* @__PURE__ */ new Set();
|
|
855
|
+
for (const hex of publishedEphPks) published.add(hex.toLowerCase());
|
|
856
|
+
let highest = -1;
|
|
857
|
+
for (let i = 0; i < window; i++) {
|
|
858
|
+
if (published.has(deriveOutgoingEphPk(outgoingViewingKey, i))) highest = i;
|
|
859
|
+
}
|
|
860
|
+
return highest + 1;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// src/protocol/note/recoverSent.ts
|
|
864
|
+
function recoverSentFromSharedSecret(hint, outgoingSharedSecret, ephIndex) {
|
|
865
|
+
if (!isHexOfLength(hint.commitmentHex, 32)) return null;
|
|
866
|
+
if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
|
|
867
|
+
try {
|
|
868
|
+
const plaintext = EncryptedMemo.decryptWithSharedSecret(
|
|
869
|
+
fromHex(hint.encryptedMemo),
|
|
870
|
+
fromHex(hint.commitmentHex),
|
|
871
|
+
outgoingSharedSecret
|
|
872
|
+
);
|
|
873
|
+
if (!plaintext) return null;
|
|
874
|
+
return {
|
|
875
|
+
commitmentHex: hint.commitmentHex,
|
|
876
|
+
...isValidLeafIndex(hint.leafIndex) ? { leafIndex: hint.leafIndex } : {},
|
|
877
|
+
value: plaintext.value,
|
|
878
|
+
assetId: plaintext.assetId,
|
|
879
|
+
// On an outgoing note the memo's ownerPk IS the recipient's stealth
|
|
880
|
+
// owner — the builder wrote it that way.
|
|
881
|
+
recipientStealthPk: plaintext.ownerPk,
|
|
882
|
+
blinding: plaintext.blinding,
|
|
883
|
+
sourcePk: plaintext.sourcePk,
|
|
884
|
+
circuitVersion: plaintext.circuitVersion,
|
|
885
|
+
ephIndex
|
|
886
|
+
};
|
|
887
|
+
} catch {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function recoverSentNote(params) {
|
|
892
|
+
const { hint, outgoingViewingKey, ephIndex, recipientCandidates } = params;
|
|
893
|
+
if (!isHexOfLength(hint.commitmentHex, 32)) return null;
|
|
894
|
+
if (!isHexOfLength(hint.encryptedMemo, ENCRYPTED_MEMO_SIZE)) return null;
|
|
895
|
+
for (const candidate of recipientCandidates) {
|
|
896
|
+
let sharedSecret;
|
|
897
|
+
try {
|
|
898
|
+
sharedSecret = deriveOutgoingSharedSecret(outgoingViewingKey, ephIndex, candidate);
|
|
899
|
+
} catch {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const facts = recoverSentFromSharedSecret(hint, sharedSecret, ephIndex);
|
|
903
|
+
if (facts) return { ...facts, recipientIvk: candidate };
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// src/protocol/note/recipientBook.ts
|
|
909
|
+
import { sha256 as sha2564 } from "@noble/hashes/sha2.js";
|
|
910
|
+
var RECIPIENT_BOOK_DOMAIN = new TextEncoder().encode("orbinum-recipient-book-v3");
|
|
911
|
+
var IVK_SIZE = 32;
|
|
912
|
+
function bookKeystream(outgoingViewingKey, paymentCommitmentHex) {
|
|
913
|
+
if (outgoingViewingKey.length !== 32) {
|
|
914
|
+
throw new Error(
|
|
915
|
+
`recipientBook: outgoingViewingKey must be 32 bytes, got ${outgoingViewingKey.length}`
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (outgoingViewingKey.every((b) => b === 0)) {
|
|
919
|
+
throw new Error(
|
|
920
|
+
"recipientBook: outgoingViewingKey is all zeros \u2014 the book would be public"
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
const normalised = paymentCommitmentHex.toLowerCase().replace(/^0x/, "");
|
|
924
|
+
const h = sha2564.create();
|
|
925
|
+
h.update(RECIPIENT_BOOK_DOMAIN);
|
|
926
|
+
h.update(outgoingViewingKey);
|
|
927
|
+
h.update(new TextEncoder().encode(normalised));
|
|
928
|
+
return h.digest();
|
|
929
|
+
}
|
|
930
|
+
function sealRecipientBookEntry(ivkPacked, outgoingViewingKey, paymentCommitmentHex) {
|
|
931
|
+
if (ivkPacked.length !== IVK_SIZE) return ivkPacked;
|
|
932
|
+
const keystream = bookKeystream(outgoingViewingKey, paymentCommitmentHex);
|
|
933
|
+
const out = new Uint8Array(IVK_SIZE);
|
|
934
|
+
for (let i = 0; i < IVK_SIZE; i++) out[i] = ivkPacked[i] ^ keystream[i];
|
|
935
|
+
return out;
|
|
936
|
+
}
|
|
937
|
+
function openRecipientBookEntry(sealedSourcePk, outgoingViewingKey, paymentCommitmentHex) {
|
|
938
|
+
if (sealedSourcePk < 0n || sealedSourcePk >> 256n !== 0n) return new Uint8Array(IVK_SIZE);
|
|
939
|
+
return sealRecipientBookEntry(
|
|
940
|
+
bigintTo32Le(sealedSourcePk),
|
|
941
|
+
outgoingViewingKey,
|
|
942
|
+
paymentCommitmentHex
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
871
946
|
// src/protocol/note/NoteDisclosure.ts
|
|
872
947
|
import { poseidon4 as poseidon43 } from "poseidon-lite";
|
|
873
948
|
|
|
@@ -953,29 +1028,41 @@ function decodeNoteDisclosureKey(key) {
|
|
|
953
1028
|
}
|
|
954
1029
|
|
|
955
1030
|
// src/protocol/eph/selfEph.ts
|
|
956
|
-
import { sha256 as
|
|
957
|
-
import { packPoint as
|
|
958
|
-
var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-
|
|
959
|
-
function deriveSelfEphSk(
|
|
960
|
-
|
|
1031
|
+
import { sha256 as sha2565 } from "@noble/hashes/sha2.js";
|
|
1032
|
+
import { packPoint as packPoint3, unpackPoint as unpackPoint3 } from "@zk-kit/baby-jubjub";
|
|
1033
|
+
var SELF_EPH_DOMAIN = new TextEncoder().encode("orbinum-self-eph-v3");
|
|
1034
|
+
function deriveSelfEphSk(viewingSecretKey, index) {
|
|
1035
|
+
if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
|
|
1036
|
+
throw new Error(`deriveSelfEphSk: index must be a u32, got ${index}`);
|
|
1037
|
+
}
|
|
1038
|
+
assertSecretKeyBytes(viewingSecretKey, "deriveSelfEphSk: viewingSecretKey");
|
|
1039
|
+
const h = sha2565.create();
|
|
961
1040
|
h.update(SELF_EPH_DOMAIN);
|
|
962
|
-
h.update(
|
|
1041
|
+
h.update(viewingSecretKey);
|
|
963
1042
|
const idx = new Uint8Array(4);
|
|
964
1043
|
new DataView(idx.buffer).setUint32(0, index >>> 0, true);
|
|
965
1044
|
h.update(idx);
|
|
966
1045
|
return h.digest();
|
|
967
1046
|
}
|
|
968
|
-
function selfEphWindow(
|
|
1047
|
+
function selfEphWindow(viewingSecretKey, ivkPacked, from, count) {
|
|
969
1048
|
const ivkPoint = unpackPoint3(bytesToBigintLE(ivkPacked));
|
|
970
1049
|
if (!ivkPoint) throw new Error("selfEphWindow: invalid viewing public key");
|
|
971
1050
|
const entries = [];
|
|
1051
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
1052
|
+
throw new Error(`selfEphWindow: from must be a non-negative integer, got ${from}`);
|
|
1053
|
+
}
|
|
1054
|
+
if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
`selfEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
972
1059
|
for (let i = from; i < from + count; i++) {
|
|
973
|
-
const scalar = bytesToBjjScalar(deriveSelfEphSk(
|
|
1060
|
+
const scalar = bytesToBjjScalar(deriveSelfEphSk(viewingSecretKey, i));
|
|
974
1061
|
const ephPk = fastMulBase(scalar);
|
|
975
1062
|
const sharedPoint = fastMulPoint(ivkPoint, scalar);
|
|
976
1063
|
entries.push({
|
|
977
1064
|
index: i,
|
|
978
|
-
ephPkHex: toHex(bigintTo32Le(
|
|
1065
|
+
ephPkHex: toHex(bigintTo32Le(packPoint3(ephPk))),
|
|
979
1066
|
sharedSecret: bigintTo32Le(sharedPoint[0])
|
|
980
1067
|
});
|
|
981
1068
|
}
|
|
@@ -983,17 +1070,22 @@ function selfEphWindow(spendingKey, ivkPacked, from, count) {
|
|
|
983
1070
|
}
|
|
984
1071
|
|
|
985
1072
|
// src/protocol/eph/pairwiseEph.ts
|
|
986
|
-
import { sha256 as
|
|
987
|
-
import { packPoint as
|
|
1073
|
+
import { sha256 as sha2566 } from "@noble/hashes/sha2.js";
|
|
1074
|
+
import { packPoint as packPoint4, unpackPoint as unpackPoint4 } from "@zk-kit/baby-jubjub";
|
|
988
1075
|
var PAIRWISE_EPH_DOMAIN = new TextEncoder().encode("orbinum-pairwise-eph-v1");
|
|
989
1076
|
function derivePairwiseSharedSecret(myViewingSk, theirIvkPacked) {
|
|
990
|
-
const theirPoint =
|
|
1077
|
+
const theirPoint = unpackUsableViewingKey(bytesToBigintLE(theirIvkPacked));
|
|
991
1078
|
if (!theirPoint) throw new Error("derivePairwiseSharedSecret: invalid viewing public key");
|
|
1079
|
+
assertSecretKeyBytes(myViewingSk, "derivePairwiseSharedSecret: myViewingSk");
|
|
992
1080
|
const shared = fastMulPoint(theirPoint, bytesToBjjScalar(myViewingSk));
|
|
993
1081
|
return bigintTo32Le(shared[0]);
|
|
994
1082
|
}
|
|
995
1083
|
function derivePairwiseEphSk(pairSecret, index) {
|
|
996
|
-
|
|
1084
|
+
if (!Number.isInteger(index) || index < 0 || index > 4294967295) {
|
|
1085
|
+
throw new Error(`derivePairwiseEphSk: index must be a u32, got ${index}`);
|
|
1086
|
+
}
|
|
1087
|
+
assertSecretKeyBytes(pairSecret, "derivePairwiseEphSk: pairSecret");
|
|
1088
|
+
const h = sha2566.create();
|
|
997
1089
|
h.update(PAIRWISE_EPH_DOMAIN);
|
|
998
1090
|
h.update(pairSecret);
|
|
999
1091
|
const idx = new Uint8Array(4);
|
|
@@ -1005,13 +1097,21 @@ function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
|
|
|
1005
1097
|
const ivkPoint = unpackPoint4(bytesToBigintLE(receiverIvkPacked));
|
|
1006
1098
|
if (!ivkPoint) throw new Error("pairwiseEphWindow: invalid viewing public key");
|
|
1007
1099
|
const entries = [];
|
|
1100
|
+
if (!Number.isInteger(from) || from < 0) {
|
|
1101
|
+
throw new Error(`pairwiseEphWindow: from must be a non-negative integer, got ${from}`);
|
|
1102
|
+
}
|
|
1103
|
+
if (!Number.isInteger(count) || count > MAX_EPH_WINDOW) {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`pairwiseEphWindow: count must be an integer <= ${MAX_EPH_WINDOW}, got ${count}`
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1008
1108
|
for (let i = from; i < from + count; i++) {
|
|
1009
1109
|
const scalar = bytesToBjjScalar(derivePairwiseEphSk(pairSecret, i));
|
|
1010
1110
|
const ephPk = fastMulBase(scalar);
|
|
1011
1111
|
const sharedPoint = fastMulPoint(ivkPoint, scalar);
|
|
1012
1112
|
entries.push({
|
|
1013
1113
|
index: i,
|
|
1014
|
-
ephPkHex: toHex(bigintTo32Le(
|
|
1114
|
+
ephPkHex: toHex(bigintTo32Le(packPoint4(ephPk))),
|
|
1015
1115
|
sharedSecret: bigintTo32Le(sharedPoint[0])
|
|
1016
1116
|
});
|
|
1017
1117
|
}
|
|
@@ -1019,11 +1119,10 @@ function pairwiseEphWindow(pairSecret, receiverIvkPacked, from, count) {
|
|
|
1019
1119
|
}
|
|
1020
1120
|
|
|
1021
1121
|
// src/protocol/keys/PrivacyKeys.ts
|
|
1022
|
-
import { hkdf as
|
|
1023
|
-
import { sha256 as
|
|
1024
|
-
import { packPoint as
|
|
1122
|
+
import { hkdf as hkdf2 } from "@noble/hashes/hkdf.js";
|
|
1123
|
+
import { sha256 as sha2567 } from "@noble/hashes/sha2.js";
|
|
1124
|
+
import { packPoint as packPoint5 } from "@zk-kit/baby-jubjub";
|
|
1025
1125
|
var IVK_DOMAIN = new TextEncoder().encode("orbinum-ivk-v1");
|
|
1026
|
-
var OVK_DOMAIN = new TextEncoder().encode("orbinum-ovk-v1");
|
|
1027
1126
|
var KEY_VERSION = "v2";
|
|
1028
1127
|
function deriveSpendingKeyFromMaster(masterBytes) {
|
|
1029
1128
|
const skBigint = BigInt(toHex(masterBytes)) % BABYJUB_SUBORDER;
|
|
@@ -1031,12 +1130,26 @@ function deriveSpendingKeyFromMaster(masterBytes) {
|
|
|
1031
1130
|
}
|
|
1032
1131
|
function deriveViewingSecretKey(spendingKey) {
|
|
1033
1132
|
const ikm = bigintTo32Le(spendingKey);
|
|
1034
|
-
return
|
|
1133
|
+
return hkdf2(sha2567, ikm, void 0, IVK_DOMAIN, 32);
|
|
1134
|
+
}
|
|
1135
|
+
var SPEND_V3_DOMAIN = new TextEncoder().encode("orbinum-spend-v3");
|
|
1136
|
+
var IVK_V3_DOMAIN = new TextEncoder().encode("orbinum-ivk-v3");
|
|
1137
|
+
var OVK_V3_DOMAIN = new TextEncoder().encode("orbinum-ovk-v3");
|
|
1138
|
+
var deriveBranch = (rootSecret, domain) => hkdf2(sha2567, rootSecret, void 0, domain, 32);
|
|
1139
|
+
function deriveSpendingKeyV3(rootSecret) {
|
|
1140
|
+
const scalar = BigInt(toHex(deriveBranch(rootSecret, SPEND_V3_DOMAIN))) % BABYJUB_SUBORDER;
|
|
1141
|
+
return scalar === 0n ? 1n : scalar;
|
|
1142
|
+
}
|
|
1143
|
+
function deriveViewingSecretKeyV3(rootSecret) {
|
|
1144
|
+
return deriveBranch(rootSecret, IVK_V3_DOMAIN);
|
|
1145
|
+
}
|
|
1146
|
+
function deriveOutgoingViewingKeyV3(rootSecret) {
|
|
1147
|
+
return deriveBranch(rootSecret, OVK_V3_DOMAIN);
|
|
1035
1148
|
}
|
|
1036
1149
|
function deriveViewingPublicKey(ivsk) {
|
|
1037
1150
|
const ivskScalar = BigInt(toHex(ivsk)) % BABYJUB_SUBORDER || 1n;
|
|
1038
1151
|
const ivkPoint = fastMulBase(ivskScalar);
|
|
1039
|
-
const packed =
|
|
1152
|
+
const packed = packPoint5(ivkPoint);
|
|
1040
1153
|
return bigintTo32Le(packed);
|
|
1041
1154
|
}
|
|
1042
1155
|
function deriveOwnerPk(spendingKey) {
|
|
@@ -1047,19 +1160,22 @@ function deriveOwnerPk(spendingKey) {
|
|
|
1047
1160
|
return 0n;
|
|
1048
1161
|
}
|
|
1049
1162
|
}
|
|
1050
|
-
function deriveOutgoingViewingKey(masterBytes) {
|
|
1051
|
-
return hkdf3(sha2566, masterBytes, void 0, OVK_DOMAIN, 32);
|
|
1052
|
-
}
|
|
1053
1163
|
|
|
1054
1164
|
// src/wallet/worker/kernel/types.ts
|
|
1055
1165
|
var SELF_EPH_WINDOW = 1024;
|
|
1056
1166
|
var PAIRWISE_EPH_WINDOW = 64;
|
|
1167
|
+
var OUTGOING_EPH_WINDOW = 64;
|
|
1057
1168
|
var EMPTY_BATCH_RESULT = {
|
|
1058
1169
|
notes: [],
|
|
1059
1170
|
tagFiltered: 0,
|
|
1060
1171
|
selfMatched: 0,
|
|
1061
1172
|
pairwiseMatched: 0,
|
|
1062
|
-
maxSelfEphIndex: null
|
|
1173
|
+
maxSelfEphIndex: null,
|
|
1174
|
+
maxOutgoingEphIndex: null,
|
|
1175
|
+
sentNotes: [],
|
|
1176
|
+
learnedRecipients: [],
|
|
1177
|
+
unmatchedSent: [],
|
|
1178
|
+
sealedBookEntries: []
|
|
1063
1179
|
};
|
|
1064
1180
|
|
|
1065
1181
|
// src/wallet/worker/kernel/ephWindow.ts
|
|
@@ -1067,23 +1183,21 @@ var cachedWindow = null;
|
|
|
1067
1183
|
function clearKnownEphWindow() {
|
|
1068
1184
|
cachedWindow = null;
|
|
1069
1185
|
}
|
|
1070
|
-
function bytesKey(bytes) {
|
|
1071
|
-
let out = "";
|
|
1072
|
-
for (const b of bytes) out += b.toString(16).padStart(2, "0");
|
|
1073
|
-
return out;
|
|
1074
|
-
}
|
|
1075
1186
|
function getKnownEphWindow(keys) {
|
|
1076
1187
|
const selfSize = keys.selfEphWindowSize ?? SELF_EPH_WINDOW;
|
|
1077
1188
|
const pairSize = keys.pairwiseWindowSize ?? PAIRWISE_EPH_WINDOW;
|
|
1189
|
+
const outSize = keys.outgoingEphWindowSize ?? OUTGOING_EPH_WINDOW;
|
|
1078
1190
|
const counterparties = keys.pairwiseCounterparties ?? [];
|
|
1079
|
-
|
|
1080
|
-
|
|
1191
|
+
const canScanOutgoing = keys.outgoingEph === true && isUsableSecretKey(keys.outgoingViewingKey);
|
|
1192
|
+
if (!keys.selfEph && !canScanOutgoing && counterparties.length === 0) return null;
|
|
1193
|
+
const cacheKey = `${keys.spendingKey.toString(16)}:${keys.selfEph ? selfSize : 0}:${pairSize}:${canScanOutgoing ? outSize : 0}:` + counterparties.map((c) => toHex(c)).join(",");
|
|
1081
1194
|
if (cachedWindow?.cacheKey === cacheKey) return cachedWindow.window;
|
|
1082
1195
|
try {
|
|
1083
1196
|
const ivkPacked = deriveViewingPublicKey(keys.viewingKey);
|
|
1084
1197
|
const byEphPk = /* @__PURE__ */ new Map();
|
|
1198
|
+
const outgoingByEphPk = /* @__PURE__ */ new Map();
|
|
1085
1199
|
if (keys.selfEph) {
|
|
1086
|
-
for (const e of selfEphWindow(keys.
|
|
1200
|
+
for (const e of selfEphWindow(keys.viewingKey, ivkPacked, 0, selfSize)) {
|
|
1087
1201
|
byEphPk.set(e.ephPkHex.toLowerCase(), {
|
|
1088
1202
|
sharedSecret: e.sharedSecret,
|
|
1089
1203
|
index: e.index,
|
|
@@ -1104,7 +1218,12 @@ function getKnownEphWindow(keys) {
|
|
|
1104
1218
|
}
|
|
1105
1219
|
}
|
|
1106
1220
|
}
|
|
1107
|
-
|
|
1221
|
+
if (canScanOutgoing) {
|
|
1222
|
+
for (const e of outgoingEphWindow(keys.outgoingViewingKey, 0, outSize)) {
|
|
1223
|
+
outgoingByEphPk.set(e.ephPkHex.toLowerCase(), e.index);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
cachedWindow = { cacheKey, window: { byEphPk, outgoingByEphPk } };
|
|
1108
1227
|
return cachedWindow.window;
|
|
1109
1228
|
} catch {
|
|
1110
1229
|
return null;
|
|
@@ -1113,21 +1232,32 @@ function getKnownEphWindow(keys) {
|
|
|
1113
1232
|
|
|
1114
1233
|
// src/wallet/worker/kernel/decryptBatch.ts
|
|
1115
1234
|
function hintEphPkHex(hint) {
|
|
1116
|
-
if (hint.ephPkHex) return hint.ephPkHex.toLowerCase();
|
|
1117
1235
|
const memo = hint.encryptedMemo;
|
|
1118
1236
|
if (!memo || memo.length < 66) return null;
|
|
1119
1237
|
return ("0x" + memo.slice(-64)).toLowerCase();
|
|
1120
1238
|
}
|
|
1239
|
+
function sealedBookEntry(note) {
|
|
1240
|
+
if (note.sourcePk === void 0 || note.sourcePk === 0n) return null;
|
|
1241
|
+
return note.sourcePk;
|
|
1242
|
+
}
|
|
1121
1243
|
function decryptHintBatch(hints, keys) {
|
|
1122
1244
|
const activation = keys.viewTagActivationLeaf ?? null;
|
|
1123
1245
|
const knownWindow = getKnownEphWindow(keys);
|
|
1246
|
+
const sentNotes = [];
|
|
1247
|
+
const learnedRecipients = /* @__PURE__ */ new Set();
|
|
1124
1248
|
let tagFiltered = 0;
|
|
1125
1249
|
let selfMatched = 0;
|
|
1126
1250
|
let pairwiseMatched = 0;
|
|
1127
1251
|
let maxSelfEphIndex = null;
|
|
1252
|
+
let maxOutgoingEphIndex = null;
|
|
1253
|
+
const sealedEntries = [];
|
|
1254
|
+
const pendingSent = [];
|
|
1255
|
+
const seenSent = /* @__PURE__ */ new Set();
|
|
1256
|
+
const unmatchedSent = [];
|
|
1128
1257
|
const notes = hints.map((hint) => {
|
|
1129
1258
|
try {
|
|
1130
|
-
const
|
|
1259
|
+
const ephPkHex = hintEphPkHex(hint) ?? "";
|
|
1260
|
+
const known = knownWindow?.byEphPk.get(ephPkHex);
|
|
1131
1261
|
if (known) {
|
|
1132
1262
|
const result2 = tryDecryptNoteVerbose(
|
|
1133
1263
|
hint,
|
|
@@ -1142,12 +1272,26 @@ function decryptHintBatch(hints, keys) {
|
|
|
1142
1272
|
if (maxSelfEphIndex === null || known.index > maxSelfEphIndex) {
|
|
1143
1273
|
maxSelfEphIndex = known.index;
|
|
1144
1274
|
}
|
|
1275
|
+
const sealed = sealedBookEntry(result2.note);
|
|
1276
|
+
if (sealed !== null) sealedEntries.push(sealed);
|
|
1145
1277
|
} else {
|
|
1146
1278
|
pairwiseMatched++;
|
|
1147
1279
|
}
|
|
1148
1280
|
return result2.note;
|
|
1149
1281
|
}
|
|
1150
1282
|
}
|
|
1283
|
+
const outgoingIndex = knownWindow?.outgoingByEphPk.get(ephPkHex);
|
|
1284
|
+
if (outgoingIndex !== void 0) {
|
|
1285
|
+
if (maxOutgoingEphIndex === null || outgoingIndex > maxOutgoingEphIndex) {
|
|
1286
|
+
maxOutgoingEphIndex = outgoingIndex;
|
|
1287
|
+
}
|
|
1288
|
+
const key = hint.commitmentHex.toLowerCase();
|
|
1289
|
+
if (!seenSent.has(key)) {
|
|
1290
|
+
seenSent.add(key);
|
|
1291
|
+
pendingSent.push({ hint, ephIndex: outgoingIndex });
|
|
1292
|
+
}
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1151
1295
|
const viewTag = activation !== null && hint.leafIndex >= activation;
|
|
1152
1296
|
const result = tryDecryptNoteVerbose(
|
|
1153
1297
|
hint,
|
|
@@ -1162,7 +1306,44 @@ function decryptHintBatch(hints, keys) {
|
|
|
1162
1306
|
return null;
|
|
1163
1307
|
}
|
|
1164
1308
|
});
|
|
1165
|
-
|
|
1309
|
+
const carried = keys.recipientCandidates ?? [];
|
|
1310
|
+
for (const { hint, ephIndex } of pendingSent) {
|
|
1311
|
+
const candidates = [
|
|
1312
|
+
...sealedEntries.map(
|
|
1313
|
+
(e) => openRecipientBookEntry(e, keys.outgoingViewingKey, hint.commitmentHex)
|
|
1314
|
+
),
|
|
1315
|
+
...carried
|
|
1316
|
+
];
|
|
1317
|
+
const sent = recoverSentNote({
|
|
1318
|
+
hint,
|
|
1319
|
+
outgoingViewingKey: keys.outgoingViewingKey,
|
|
1320
|
+
ephIndex,
|
|
1321
|
+
recipientCandidates: candidates
|
|
1322
|
+
});
|
|
1323
|
+
if (!sent) {
|
|
1324
|
+
unmatchedSent.push({ hint, ephIndex });
|
|
1325
|
+
continue;
|
|
1326
|
+
}
|
|
1327
|
+
const { recipientIvk, ...facts } = sent;
|
|
1328
|
+
learnedRecipients.add(toHex(recipientIvk));
|
|
1329
|
+
sentNotes.push({
|
|
1330
|
+
...facts,
|
|
1331
|
+
counterpartyIvkHex: toHex(recipientIvk),
|
|
1332
|
+
encryptedMemo: hint.encryptedMemo ?? ""
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
return {
|
|
1336
|
+
notes,
|
|
1337
|
+
tagFiltered,
|
|
1338
|
+
selfMatched,
|
|
1339
|
+
pairwiseMatched,
|
|
1340
|
+
maxSelfEphIndex,
|
|
1341
|
+
maxOutgoingEphIndex,
|
|
1342
|
+
sentNotes,
|
|
1343
|
+
learnedRecipients: [...learnedRecipients],
|
|
1344
|
+
unmatchedSent,
|
|
1345
|
+
sealedBookEntries: sealedEntries.map((e) => e.toString())
|
|
1346
|
+
};
|
|
1166
1347
|
}
|
|
1167
1348
|
|
|
1168
1349
|
// src/foundation/errors/abort.ts
|
|
@@ -1197,21 +1378,52 @@ function createMainThreadPool() {
|
|
|
1197
1378
|
let selfMatched = 0;
|
|
1198
1379
|
let pairwiseMatched = 0;
|
|
1199
1380
|
let maxSelfEphIndex = null;
|
|
1381
|
+
let maxOutgoingEphIndex = null;
|
|
1382
|
+
const sentNotes = [];
|
|
1383
|
+
const unmatchedSent = [];
|
|
1384
|
+
const sealedBookEntries = /* @__PURE__ */ new Set();
|
|
1385
|
+
const learnedRecipients = new Set(
|
|
1386
|
+
(keys.recipientCandidates ?? []).map((c) => toHex(c))
|
|
1387
|
+
);
|
|
1200
1388
|
for (let i = 0; i < hints.length; i += DECRYPT_YIELD_EVERY) {
|
|
1201
1389
|
if (i > 0) {
|
|
1202
1390
|
await yieldToBrowser();
|
|
1203
1391
|
if (signal?.aborted) throw scanAbortError();
|
|
1204
1392
|
}
|
|
1205
|
-
const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY),
|
|
1393
|
+
const burst = decryptHintBatch(hints.slice(i, i + DECRYPT_YIELD_EVERY), {
|
|
1394
|
+
...keys,
|
|
1395
|
+
recipientCandidates: [...learnedRecipients].map((h) => fromHex(h))
|
|
1396
|
+
});
|
|
1206
1397
|
notes.push(...burst.notes);
|
|
1207
1398
|
tagFiltered += burst.tagFiltered;
|
|
1208
1399
|
selfMatched += burst.selfMatched;
|
|
1209
1400
|
pairwiseMatched += burst.pairwiseMatched;
|
|
1401
|
+
sentNotes.push(...burst.sentNotes ?? []);
|
|
1402
|
+
unmatchedSent.push(...burst.unmatchedSent ?? []);
|
|
1403
|
+
for (const e of burst.sealedBookEntries ?? []) sealedBookEntries.add(e);
|
|
1404
|
+
for (const r of burst.learnedRecipients ?? []) learnedRecipients.add(r);
|
|
1210
1405
|
if (burst.maxSelfEphIndex !== null) {
|
|
1211
1406
|
maxSelfEphIndex = Math.max(maxSelfEphIndex ?? -1, burst.maxSelfEphIndex);
|
|
1212
1407
|
}
|
|
1408
|
+
if (burst.maxOutgoingEphIndex != null) {
|
|
1409
|
+
maxOutgoingEphIndex = Math.max(
|
|
1410
|
+
maxOutgoingEphIndex ?? -1,
|
|
1411
|
+
burst.maxOutgoingEphIndex
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1213
1414
|
}
|
|
1214
|
-
return {
|
|
1415
|
+
return {
|
|
1416
|
+
notes,
|
|
1417
|
+
tagFiltered,
|
|
1418
|
+
selfMatched,
|
|
1419
|
+
pairwiseMatched,
|
|
1420
|
+
maxSelfEphIndex,
|
|
1421
|
+
maxOutgoingEphIndex,
|
|
1422
|
+
sentNotes,
|
|
1423
|
+
learnedRecipients: [...learnedRecipients],
|
|
1424
|
+
unmatchedSent,
|
|
1425
|
+
sealedBookEntries: [...sealedBookEntries]
|
|
1426
|
+
};
|
|
1215
1427
|
},
|
|
1216
1428
|
terminate() {
|
|
1217
1429
|
clearKnownEphWindow();
|
|
@@ -1243,7 +1455,12 @@ function runOnWorker(worker, payload, signal) {
|
|
|
1243
1455
|
tagFiltered: data.tagFiltered ?? 0,
|
|
1244
1456
|
selfMatched: data.selfMatched ?? 0,
|
|
1245
1457
|
pairwiseMatched: data.pairwiseMatched ?? 0,
|
|
1246
|
-
maxSelfEphIndex: data.maxSelfEphIndex ?? null
|
|
1458
|
+
maxSelfEphIndex: data.maxSelfEphIndex ?? null,
|
|
1459
|
+
maxOutgoingEphIndex: data.maxOutgoingEphIndex ?? null,
|
|
1460
|
+
sentNotes: data.sentNotes ?? [],
|
|
1461
|
+
learnedRecipients: data.learnedRecipients ?? [],
|
|
1462
|
+
unmatchedSent: data.unmatchedSent ?? [],
|
|
1463
|
+
sealedBookEntries: data.sealedBookEntries ?? []
|
|
1247
1464
|
});
|
|
1248
1465
|
};
|
|
1249
1466
|
worker.onerror = () => {
|
|
@@ -1270,9 +1487,19 @@ function mergeResults(results) {
|
|
|
1270
1487
|
tagFiltered: results.reduce((sum, r) => sum + r.tagFiltered, 0),
|
|
1271
1488
|
selfMatched: results.reduce((sum, r) => sum + r.selfMatched, 0),
|
|
1272
1489
|
pairwiseMatched: results.reduce((sum, r) => sum + r.pairwiseMatched, 0),
|
|
1490
|
+
sentNotes: results.flatMap((r) => r.sentNotes ?? []),
|
|
1491
|
+
// Deduped: slices run in parallel, so the same change note cannot be
|
|
1492
|
+
// seen twice, but two different ones may name the same recipient.
|
|
1493
|
+
learnedRecipients: [...new Set(results.flatMap((r) => r.learnedRecipients ?? []))],
|
|
1494
|
+
unmatchedSent: results.flatMap((r) => r.unmatchedSent ?? []),
|
|
1495
|
+
sealedBookEntries: [...new Set(results.flatMap((r) => r.sealedBookEntries ?? []))],
|
|
1273
1496
|
maxSelfEphIndex: results.reduce(
|
|
1274
1497
|
(max, r) => r.maxSelfEphIndex === null ? max : Math.max(max ?? -1, r.maxSelfEphIndex),
|
|
1275
1498
|
null
|
|
1499
|
+
),
|
|
1500
|
+
maxOutgoingEphIndex: results.reduce(
|
|
1501
|
+
(max, r) => r.maxOutgoingEphIndex == null ? max : Math.max(max ?? -1, r.maxOutgoingEphIndex),
|
|
1502
|
+
null
|
|
1276
1503
|
)
|
|
1277
1504
|
};
|
|
1278
1505
|
}
|
|
@@ -1339,10 +1566,14 @@ export {
|
|
|
1339
1566
|
BN254_R,
|
|
1340
1567
|
BABYJUB_SUBORDER,
|
|
1341
1568
|
recoverOwnerPkPoint,
|
|
1569
|
+
unpackUsableViewingKey,
|
|
1570
|
+
isUsableSecretKey,
|
|
1571
|
+
assertSecretKeyBytes,
|
|
1342
1572
|
fastMulBase,
|
|
1343
1573
|
fastMulPoint,
|
|
1344
1574
|
deriveStealthOwnerPk,
|
|
1345
1575
|
deriveStealthSk,
|
|
1576
|
+
randomBlinding,
|
|
1346
1577
|
scanAbortError,
|
|
1347
1578
|
isAbortError,
|
|
1348
1579
|
serializeMemo,
|
|
@@ -1350,11 +1581,6 @@ export {
|
|
|
1350
1581
|
ENCRYPTED_MEMO_SIZE,
|
|
1351
1582
|
bytesToBjjScalar,
|
|
1352
1583
|
EncryptedMemo,
|
|
1353
|
-
OVK_BLOB_SIZE,
|
|
1354
|
-
deriveOutgoingCipherKey,
|
|
1355
|
-
sealOutgoingBlob,
|
|
1356
|
-
openOutgoingBlob,
|
|
1357
|
-
randomOutgoingBlob,
|
|
1358
1584
|
LEAVES_PER_TREE,
|
|
1359
1585
|
isValidLeafIndex,
|
|
1360
1586
|
treeIdOf,
|
|
@@ -1367,6 +1593,11 @@ export {
|
|
|
1367
1593
|
derivePairwiseSharedSecret,
|
|
1368
1594
|
derivePairwiseEphSk,
|
|
1369
1595
|
pairwiseEphWindow,
|
|
1596
|
+
deriveOutgoingEphSk,
|
|
1597
|
+
deriveOutgoingEphPk,
|
|
1598
|
+
outgoingEphWindow,
|
|
1599
|
+
deriveOutgoingSharedSecret,
|
|
1600
|
+
reconstructOutgoingIndex,
|
|
1370
1601
|
CURRENT_CIRCUIT_VERSION,
|
|
1371
1602
|
NoteBuilder,
|
|
1372
1603
|
computeNullifier,
|
|
@@ -1374,18 +1605,24 @@ export {
|
|
|
1374
1605
|
computeNoteCommitment,
|
|
1375
1606
|
tryDecryptNote,
|
|
1376
1607
|
tryDecryptNoteVerbose,
|
|
1377
|
-
tryRecoverOutgoing,
|
|
1378
1608
|
collectOutgoingFacts,
|
|
1609
|
+
recoverSentFromSharedSecret,
|
|
1610
|
+
recoverSentNote,
|
|
1611
|
+
sealRecipientBookEntry,
|
|
1612
|
+
openRecipientBookEntry,
|
|
1379
1613
|
createNoteDisclosureKey,
|
|
1380
1614
|
decodeNoteDisclosureKey,
|
|
1381
1615
|
KEY_VERSION,
|
|
1382
1616
|
deriveSpendingKeyFromMaster,
|
|
1383
1617
|
deriveViewingSecretKey,
|
|
1618
|
+
deriveSpendingKeyV3,
|
|
1619
|
+
deriveViewingSecretKeyV3,
|
|
1620
|
+
deriveOutgoingViewingKeyV3,
|
|
1384
1621
|
deriveViewingPublicKey,
|
|
1385
1622
|
deriveOwnerPk,
|
|
1386
|
-
deriveOutgoingViewingKey,
|
|
1387
1623
|
SELF_EPH_WINDOW,
|
|
1388
1624
|
PAIRWISE_EPH_WINDOW,
|
|
1625
|
+
OUTGOING_EPH_WINDOW,
|
|
1389
1626
|
EMPTY_BATCH_RESULT,
|
|
1390
1627
|
clearKnownEphWindow,
|
|
1391
1628
|
getKnownEphWindow,
|