@ninsei-labs/sdk 0.39.3 → 0.40.0-develop.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/engine/atomic/dleq2.js +86 -111
  3. package/engine/atomic/halfenc.js +58 -72
  4. package/engine/atomic/halves.js +31 -53
  5. package/engine/atomic/order-client.js +15 -16
  6. package/engine/atomic/order.js +44 -62
  7. package/engine/atomic/orderContext.js +26 -50
  8. package/engine/atomic/orderHalfCommit.js +17 -18
  9. package/engine/core/chainSource.js +54 -69
  10. package/engine/core/clock.js +11 -12
  11. package/engine/core/config.js +554 -449
  12. package/engine/core/finality.js +34 -53
  13. package/engine/core/format.js +35 -36
  14. package/engine/core/preSignGuards.js +109 -117
  15. package/engine/core/rfqSource.js +57 -75
  16. package/engine/core/sellWindow.js +23 -24
  17. package/engine/core/store.js +20 -21
  18. package/engine/core/swap.js +242 -243
  19. package/engine/evm/amounts.js +50 -51
  20. package/engine/evm/asyncExec.js +577 -0
  21. package/engine/evm/asyncLegs.js +308 -0
  22. package/engine/evm/asyncRoute.js +179 -0
  23. package/engine/evm/auth.js +41 -48
  24. package/engine/evm/claimGas.js +28 -47
  25. package/engine/evm/depositor.js +39 -43
  26. package/engine/evm/dex.js +266 -267
  27. package/engine/evm/escrow.js +113 -129
  28. package/engine/evm/factory.js +71 -77
  29. package/engine/evm/fees.js +41 -42
  30. package/engine/evm/funding.js +160 -162
  31. package/engine/evm/gasReserve.js +67 -36
  32. package/engine/evm/index.js +46 -53
  33. package/engine/evm/injected.js +34 -42
  34. package/engine/evm/permit.js +129 -56
  35. package/engine/evm/prices.js +30 -45
  36. package/engine/evm/session.js +188 -189
  37. package/engine/mock/chain.js +17 -18
  38. package/engine/monero/address.js +33 -48
  39. package/engine/monero/halvesSweep.js +25 -43
  40. package/engine/monero/node.js +14 -15
  41. package/engine/monero/scan.js +82 -95
  42. package/engine/monero/swapKeys.js +30 -33
  43. package/engine/monero/unlock.js +17 -20
  44. package/engine/monero/wallet.js +32 -42
  45. package/engine/recovery/recoveryFile.js +154 -161
  46. package/engine/recovery/restore.js +50 -61
  47. package/engine/sdk/bridge.js +244 -234
  48. package/engine/ui/dom.js +4 -5
  49. package/engine/ui/walletModal.js +20 -31
  50. package/index.d.ts +8 -0
  51. package/package.json +1 -1
  52. package/src/adapters/evm-wallet.mjs +16 -0
  53. package/src/adapters/monero-wallet.mjs +1 -1
  54. package/src/adapters/monero.mjs +1 -1
  55. package/src/adapters/node-test-evm-wallet.mjs +5 -5
  56. package/src/config.mjs +1 -1
  57. package/src/engine.mjs +4 -0
  58. package/src/legs/cow-spec.mjs +254 -0
  59. package/src/legs/cow.mjs +315 -0
  60. package/src/legs/evm.mjs +86 -2
  61. package/src/legs/kyber-spec.mjs +265 -0
  62. package/src/legs/kyber.mjs +425 -0
  63. package/src/legs/shape.mjs +35 -0
  64. package/src/legs/uniswaprelay-spec.mjs +541 -0
  65. package/src/legs/uniswaprelay.mjs +422 -0
  66. package/src/legs/uniswapx-spec.mjs +329 -0
  67. package/src/legs/uniswapx.mjs +362 -0
  68. package/src/quotes.mjs +1 -1
  69. package/src/swaps.mjs +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,38 @@
1
+ ## 0.40.0-develop.1 — 2026-10-10
2
+
3
+ A build of the asynchronous route-provider work, published for the demo and for our own testing. This build does NOT
4
+ move `latest`: install it as `@ninsei-labs/sdk@develop`, or pin this exact version.
5
+
6
+ - Four asynchronous route providers: CoWSwap (settles native), KyberSwap Limit Order (settles wrapped), UniswapX
7
+ (settles native; publishing needs a Uniswap Trading API key and a submitter URL of your own), and the relayed
8
+ Uniswap route through the UniversalRouter.
9
+ - `submitUniswapxOrder` publishes through a submitter you name; the package never carries an API key.
10
+ - A route can be composed of two legs - liquidity to the wrapped token, then a native leg - and a provider that
11
+ cannot carry the route to what the person needs can never be chosen alone.
12
+ - The engine mirror is regenerated from the source of truth, and the vendored browser bundle is rebuilt.
13
+
14
+ Nothing that already existed changed behaviour: the synchronous swap path is untouched.
15
+
16
+ ## Unreleased — 2026-10-09
17
+
18
+ THE ENGINE MIRROR IS SYNCED AGAIN AFTER THE DEMO'S BUNDLE REBUILD.
19
+
20
+ - All 45 `engine/**` modules are byte-for-byte copies of the demo repository's `sdk/engine` (regenerated by
21
+ the demo's `tools/build-sdk-engine.mjs`). Two files changed since the previous sync, and only in the asset
22
+ version stamp: `engine/recovery/restore.js` (`atomic-browser.js?v=77cc4725`) and `engine/sdk/bridge.js`
23
+ (`sdk-browser.js?v=fbc82629`) — the demo's bundles were rebuilt after the repository-wide brand rename
24
+ (`Arrakis` -> `Ninsei`, English comments), so their content hashes moved.
25
+ - No API, code table or message changed: this entry records a mirror refresh, not a behaviour change.
26
+
27
+ ## Unreleased
28
+
29
+ THE ENGINE MIRROR (`engine/**`) CROSSES OVER IN ENGLISH, WITHOUT THE OLD BRAND AND WITHOUT A HARDCODED DOMAIN.
30
+ All 45 modules are copied byte-for-byte from the demo repository's `sdk/engine` (`tools/build-sdk-engine.mjs`):
31
+ comments are concise technical English and message strings are translated one to one, `Arrakis` is renamed to
32
+ `Ninsei` (including environment constants and paths), the hardcoded domain is gone from the code - the page's own
33
+ address comes from `location.origin` and node addresses from one named place set at deploy - and the EIP-712
34
+ signing-domain name/version are read from the config, not from a literal in the signer.
35
+
1
36
  ## 0.39.3 — 2026-10-09
2
37
 
3
38
  A NODE'S ADDRESS DENYLIST REFUSAL REACHES THE APP AS ITS OWN CODE, AND THE INTERFACE CAN CHECK ADDRESSES ITSELF (issue #16).
@@ -1,32 +1,27 @@
1
1
  // GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/dleq2.js for the SDK package.
2
- // Edit the source under www/js, then run: node tools/build-sdk-engine.mjs
3
- // tools/check-sdk-engine.mjs reddens on any drift, so a stale copy cannot ship silently.
2
+ // Edit the source under www/js and regenerate the mirror; a drift guard reddens on any difference.
4
3
  // Everything below this header is identical to the source.
5
4
 
6
- // DLEQ между кривыми: доказательство, что за точками на secp256k1 и ed25519 стоит ОДИН скаляр.
7
- // Перенос конструкции из github.com/athanorlabs/go-dleq (prove.go, verify.go) на @noble/curves.
8
- // Разбор алгоритма: .hermes/docs/20-key-shares.md, разделы 14-16.
5
+ // DLEQ ACROSS CURVES: a proof that one scalar stands behind points on secp256k1 and ed25519.
6
+ // Ported from go-dleq (prove.go, verify.go) to @noble/curves.
7
+ // THE CHALLENGE IS COMPUTED AS IN THE ORIGINAL: SHA3-512 and WIDE REDUCTION. The old edition used keccak256
8
+ // (32 bytes) reduced mod the order, which was BIASED (~2^-128) and differed from the reference; named by the
9
+ // audit and closed here. Now: a 64-byte hash, read LEAST-SIGNIFICANT first and reduced mod the order, exactly
10
+ // what ed25519 HashToScalar does. CONSEQUENCE: old proofs do not verify, so the context version was raised
11
+ // (ninsei-order-v3), else a live quote with an old proof would break silently.
12
+ // STRICT SUBGROUP CHECK. ed25519 has cofactor 8, so a point may carry a small-order component; the check
13
+ // "multiply by 8 is non-zero" lets it through and makes the equalities degenerate. We check strictly: [l]*P = 0.
14
+ // IMPORTANT: the primitives are audited (@noble/curves), THIS implementation is not.
9
15
  //
10
- // ВЫЗОВ СЧИТАЕТСЯ ТАК ЖЕ, КАК В ОРИГИНАЛЕ: SHA3-512 И ШИРОКОЕ ПРИВЕДЕНИЕ. Прежняя редакция считала вызов
11
- // keccak256, то есть 32 байта, и приводила их по модулю порядка - приведение было СМЕЩЕНО (около 2^-128) и
12
- // отличалось от эталона; это названо аудитом (reports/audit-02/review_02-dleq2.md §5.1) и закрыто здесь.
13
- // Теперь: хеш 64 байта, читается МЛАДШИМ БАЙТОМ ВПЕРЁД и приводится по модулю порядка - ровно то, что
14
- // делает edwards25519 Scalar.SetUniformBytes в go-dleq (ed25519_curve.go, HashToScalar).
15
- // СЛЕДСТВИЕ НАЗВАНО ПРЯМО: доказательства прежнего варианта не проходят, поэтому поднята версия контекста
16
- // (arrakis-order-v3) - иначе живая котировка со старым доказательством молча ломалась бы.
17
16
  //
18
- // СТРОГАЯ ПРОВЕРКА ПОДГРУППЫ. У ed25519 кофактор 8, и точка может нести составляющую малого порядка:
19
- // проверка «умножение на 8 не даёт ноль» её пропускает, а именно она делает равенства вырожденными.
20
- // Проверяем строго: [l]*P = 0, то есть точка лежит в подгруппе простого порядка.
21
17
  //
22
- // ВАЖНО: примитивы аудированы (@noble/curves), ЭТА реализация - нет.
23
18
 
24
19
  export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomBytes }) {
25
- // SHA3-512 ОБЯЗАТЕЛЕН: без него считать вызов нечем, а молчаливая подмена на keccak256 была бы ровно
26
- // той ошибкой, которую здесь закрывают. Отказ громкий и сразу.
27
- if (typeof sha3_512 !== "function") throw new Error("createDleq2: нужна функция sha3_512 (SHA3-512 из @noble/hashes)");
20
+ // SHA3-512 IS MANDATORY: without it the challenge cannot be computed, and a silent fallback to keccak256 would
21
+ // be exactly the bug closed here. The refusal is loud and immediate.
22
+ if (typeof sha3_512 !== "function") throw new Error("createDleq2: sha3_512 is required (SHA3-512 from @noble/hashes)");
28
23
  const ED_ORDER = ed25519.Point.Fn.ORDER, SEC_ORDER = secp256k1.Point.Fn.ORDER;
29
- // Секрет живёт числом бит МЕНЬШЕГО порядка: так снимается ограничение ks < min(l, n).
24
+ // The secret lives in the bit length of the SMALLER order: this lifts the ks < min(l, n) limit.
30
25
  const BITS = Math.min(ED_ORDER.toString(2).length, SEC_ORDER.toString(2).length);
31
26
 
32
27
  const curves = {
@@ -42,28 +37,23 @@ export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomByt
42
37
  },
43
38
  };
44
39
 
45
- // ПРЕДВЫЧИСЛЕНИЯ ДЛЯ ВТОРОЙ БАЗОВОЙ ТОЧКИ. Она умножается в каждом бите четыре раза, плюс раз на
46
- // бит в обязательствах, то есть около тысячи раз на доказательство - и всё это время умножение шло
47
- // БЕЗ таблицы. Измерено (tools/measure-proof-components.mjs, отдельный замер предвычислений):
48
- // без таблицы: 2,84 мс (secp256k1) и 2,86 мс (ed25519)
49
- // W=8: 0,905 мс (3,1x) и 0,422 мс (6,8x); построение таблицы 226 мс, один раз на процесс
50
- // W=10 даёт ещё 13%, но построение втрое дороже (732 мс), а W=12 уже 2,4 с - на старте это заметно.
51
- // true означает ленивость: здесь только запоминается размер окна, таблица построится при первом
52
- // умножении. Поэтому импорт модуля не дорожает, а платит первый расчёт - и платит один раз.
40
+ // PRECOMPUTATION FOR THE SECOND BASE POINT. It is multiplied four times per bit, plus once in the commitments -
41
+ // about a thousand times per proof, all of it WITHOUT a table. Measured: without a table 2.84 ms (secp256k1) and
42
+ // 2.86 ms (ed25519); W=8 gives 0.905 ms and 0.422 ms with a table built once (226 ms). W=10 adds 13% but builds
43
+ // 3x slower; W=12 is 2.4 s. true means lazy: the table is built at the first multiplication, so importing the
44
+ // module is cheap and the first computation pays once.
53
45
  curves.A.Galt.precompute(8);
54
46
  curves.B.Galt.precompute(8);
55
47
 
56
- // КОНТЕКСТ ИДЁТ СЫРЫМИ БАЙТАМИ UTF-8. Прежний ctxHex собирал hex из charCodeAt и отдавал его в hexBytes:
57
- // для ASCII это ровно те же байты, но для любого символа выше 0x7F hex выходил длиннее двух цифр, hexBytes
58
- // резал его по чётности, и доказывающий с проверяющим могли посчитать РАЗНЫЕ вызовы. Канонический контекст
59
- // ордера - ASCII, поэтому живые котировки не меняются: это проверено сравнением байт (tools/check-dleq2.mjs).
60
- // Кодировщик свой, а не TextEncoder: он не требует глобального объекта в среде воркера страницы.
48
+ // THE CONTEXT GOES IN AS RAW UTF-8 BYTES. The old ctxHex built hex from charCodeAt, which broke for any char
49
+ // above 0x7F (the prover and verifier could compute DIFFERENT challenges). The canonical order context is ASCII,
50
+ // so live quotes do not change. The encoder is our own, not TextEncoder, to avoid a global in the worker.
61
51
  const ctxUtf8 = (context) => {
62
52
  const t = String(context == null ? "" : context);
63
53
  const out = [];
64
54
  for (let i = 0; i < t.length; i++) {
65
55
  let c = t.codePointAt(i);
66
- if (c > 0xffff) i++; // суррогатная пара: codePointAt уже вернул её целиком
56
+ if (c > 0xffff) i++; // surrogate pair: codePointAt already returned it whole
67
57
  if (c < 0x80) out.push(c);
68
58
  else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
69
59
  else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
@@ -80,59 +70,50 @@ export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomByt
80
70
  }
81
71
  const bigToHex = (v) => v.toString(16).padStart(64, "0");
82
72
  function bytesToBig(b) { let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v; }
83
- // ЧТЕНИЕ МЛАДШИМ БАЙТОМ ВПЕРЁД - как у edwards25519 в эталоне. Это не стилистика: с обратным порядком
84
- // байт вызов получился бы другим, и доказательство не сошлось бы с реализацией на другой стороне.
73
+ // READ LEAST-SIGNIFICANT FIRST, as ed25519 in the reference. Not style: the reverse order gives a different
74
+ // challenge and the proof would not match the other side.
85
75
  function bytesToBigLE(b) { let v = 0n; for (let i = b.length - 1; i >= 0; i--) v = (v << 8n) | BigInt(b[i]); return v; }
86
76
  function concat(parts) {
87
77
  const t = parts.reduce((s, p) => s + p.length, 0), out = new Uint8Array(t);
88
78
  let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } return out;
89
79
  }
90
- // Вызов: один и тот же preimage, но приведение к скаляру - по правилам СВОЕЙ кривой.
91
- // ПРИВЯЗКА К ОРДЕРУ. Контекст (например, адрес эскроу и hashlock) входит в preimage вызова.
92
- // Без него доказательство можно было бы переиспользовать в другом ордере с той же половиной:
93
- // в вызове участвовали только точки и обязательства. Найдено при сверке документов, раздел 21.2.
94
- // Контекст ордера НЕ здесь: он уже входит в parts через сборку preimage (см. ctxUtf8). Одна механика
95
- // на обе стороны - иначе доказывающий и проверяющий считали бы вызов по-разному (так и вышло в первом
96
- // варианте правки: параметр был добавлен в сигнатуру, а подставлен в один вызов из двенадцати).
80
+ // Challenge: the same preimage, but reduced to a scalar by each curve's own rules. The ORDER CONTEXT is not
81
+ // here - it enters parts via the preimage assembly. One mechanic for both sides, else the prover and verifier
82
+ // would compute different challenges.
97
83
  function challenge(curve, parts) {
98
84
  const h = sha3_512(concat(parts.map((p) => (typeof p === "string" ? hexBytes(p) : p))));
99
- if (h.length !== 64) throw new Error("challenge: sha3_512 вернул " + h.length + " байт вместо 64");
85
+ if (h.length !== 64) throw new Error("challenge: sha3_512 returned " + h.length + " bytes instead of 64");
100
86
  return bytesToBigLE(h) % curve.order;
101
87
  }
102
- // СЛУЧАЙНЫЙ СКАЛЯР - ТОЖЕ ШИРОКОЕ ПРИВЕДЕНИЕ: 64 случайных байта по модулю порядка, как NewRandomScalar
103
- // в эталоне. Прежний цикл брал 32 байта и отбраковывал всё, что не легло в порядок: смещение около
104
- // 2^-128 и лишние повторы. Ноль теоретически возможен (порядка 2^-252) и здесь заменяется единицей:
105
- // для обязательств это значит лишь ненулевую случайность, а не отказ.
88
+ // A RANDOM SCALAR IS ALSO WIDE-REDUCED: 64 random bytes mod the order. The old loop took 32 bytes and rejected
89
+ // anything out of range: biased (~2^-128). Zero is theoretically possible (about 2^-252) and is replaced by one
90
+ // - for commitments that is just non-zero randomness, not a refusal.
106
91
  function randScalar(curve) {
107
92
  const v = bytesToBigLE(randomBytes(64)) % curve.order;
108
93
  return v === 0n ? 1n : v;
109
94
  }
110
95
  function bit(x, i) { return (x >> BigInt(i)) & 1n; }
111
- // Умножение на ПУБЛИЧНЫЙ скаляр. В доказательстве DLEQ все скаляры и так входят в само доказательство,
112
- // которое уходит контрагенту, поэтому постоянное время здесь не защищает ничего, а стоит дорого:
113
- // измерено multiply 3.1 мс против multiplyUnsafe 0.62 мс на ed25519 (3.5 против 0.78 на secp256k1).
114
- // На бит приходится около десятка умножений, и разница составляла основную часть тех 20 секунд.
96
+ // Multiplication by a PUBLIC scalar. In DLEQ all scalars are part of the proof the counterparty receives, so
97
+ // constant time protects nothing and costs a lot: multiply 3.1 ms vs multiplyUnsafe 0.62 ms on ed25519. About
98
+ // ten multiplications per bit, and the difference was most of those 20 seconds.
115
99
  //
116
- // НОЛЬ ЗДЕСЬ ЗАКОНЕН. Это первое, что нужно знать об этой функции: нулевой бит даёт b*G при b=0, и
117
- // приведённый к нулю вызов тоже встречается. Прежний код возвращал в этом случае нейтральный элемент,
118
- // и это правильное поведение. Я однажды заменил его проверкой, которая бросала исключение, - и получил
119
- // падение на ровном месте, потому что комментарий прежнего кода не прочитал, а только переписал.
120
- // Ноль возвращает нейтральный элемент кривой, multiplyUnsafe(0) не вызывается вовсе: он бросает.
100
+ // ZERO IS LEGAL HERE. A zero bit gives b*G at b=0, and a zero-reduced challenge also occurs. The old code
101
+ // returned the neutral element, which is correct. Zero returns the curve neutral element; multiplyUnsafe(0) is
102
+ // not called at all - it throws.
121
103
  function mPublic(curve, point, scalar) {
122
- // Скаляр приходит и числом, и hex-строкой: в доказательстве скаляры хранятся hex, чтобы переживать
123
- // JSON. Обе формы приводятся здесь, в одном месте.
104
+ // The scalar arrives as a number or a hex string: scalars are stored as hex to survive JSON, and both forms
105
+ // are normalised here.
124
106
  const raw = typeof scalar === "bigint" ? scalar : BigInt(scalar);
125
107
  const s = ((raw % curve.order) + curve.order) % curve.order;
126
108
  if (s === 0n) return curve.P.ZERO;
127
- // ТОЧКА С ТАБЛИЦЕЙ УМНОЖАЕТСЯ ЧЕРЕЗ multiply, остальные - через multiplyUnsafe. Это не взаимозаменяемо:
128
- // multiplyUnsafe таблицу не использует и на второй базовой точке остаётся втрое-вшестеро дороже, а
129
- // multiply БЕЗ таблицы, наоборот, дороже multiplyUnsafe (измерено 7,1 мс против 2,8). Поэтому выбор
130
- // привязан к конкретной точке, а не к удобству вызова.
109
+ // A POINT WITH A TABLE IS MULTIPLIED VIA multiply, the rest via multiplyUnsafe. Not interchangeable:
110
+ // multiplyUnsafe ignores the table and stays 3-6x costlier on the second base point, while multiply WITHOUT
111
+ // a table is costlier than multiplyUnsafe (measured 7.1 ms vs 2.8). The choice is tied to the point.
131
112
  if (point === curve.Galt) return point.multiply(s);
132
113
  return point.multiplyUnsafe(s);
133
114
  }
134
115
 
135
- // Точка из скаляра на второй базовой точке кривой.
116
+ // Point from a scalar on the second base point of the curve.
136
117
  function mul(curve, s, base) {
137
118
  return mPublic(curve, base || curve.G, typeof s === "bigint" ? s : BigInt(s));
138
119
  }
@@ -141,7 +122,7 @@ export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomByt
141
122
  const encP = (p) => hexBytes(p.toHex());
142
123
  const encS = (s) => hexBytes(bigToHex(s));
143
124
 
144
- // Обязательства по битам: C_i = b_i*G + r_i*G' , причём Σ r_i*2^i = 0, поэтому Σ C_i*2^i = x*G.
125
+ // Per-bit commitments: C_i = b_i*G + r_i*G', with sum(r_i*2^i) = 0, so sum(C_i*2^i) = x*G.
145
126
  function commitments(curve, x) {
146
127
  const rs = [], cs = [];
147
128
  let sum = 0n, pow = 1n;
@@ -162,7 +143,7 @@ export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomByt
162
143
  function modInv(a, m) {
163
144
  let [old_r, r] = [a % m, m], [old_s, s] = [1n, 0n];
164
145
  while (r !== 0n) { const q = old_r / r; [old_r, r] = [r, old_r - q * r]; [old_s, s] = [s, old_s - q * s]; }
165
- if (old_r !== 1n) throw new Error("modInv: не обратим");
146
+ if (old_r !== 1n) throw new Error("modInv: not invertible");
166
147
  return ((old_s % m) + m) % m;
167
148
  }
168
149
  function sumCommitments(curve, cs) {
@@ -171,11 +152,10 @@ export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomByt
171
152
  return sum;
172
153
  }
173
154
 
174
- // OR-доказательство «обязательство к 0 или к 1» для одного бита, по обеим кривым сразу.
155
+ // OR proof "commitment to 0 or to 1" for one bit, on both curves at once.
175
156
  //
176
- // ПРИНИМАЕТ БИТ, А НЕ ПОЛОВИНУ, и это не украшательство: биту нужен ровно один бит половины и его
177
- // случайности. Из-за этого шаг САМОДОСТАТОЧЕН - ни половины, ни данных других битов он не требует, а
178
- // значит именно его можно отдать другому потоку, не передавая туда ключ целиком.
157
+ // IT TAKES A BIT, NOT A HALF: a bit needs exactly one bit of the half and its randomness. So the step is
158
+ // self-contained - it needs neither the half nor other bits, so it can go to another thread without the key.
179
159
  function ringSig(bitValue, i, cA, cB, rA, rB, context) {
180
160
  const j = randScalar(curves.A), k = randScalar(curves.B);
181
161
  const pre = (p1, p2) => [ctxUtf8(context), encP(cA), encP(cB), encP(p1), encP(p2)];
@@ -199,13 +179,12 @@ export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomByt
199
179
  return { eA, eB, a0: (j + eA1 * rA) % curves.A.order, a1, b0: (k + eB1 * rB) % curves.B.order, b1 };
200
180
  }
201
181
 
202
- // Скаляры уходят в hex: доказательство должно переживать JSON и границу потока - иначе его не передать.
182
+ // Scalars go to hex: the proof must survive JSON and the thread boundary.
203
183
  const sigOut = (s) => ({ eA: bigToHex(s.eA), eB: bigToHex(s.eB), a0: bigToHex(s.a0), a1: bigToHex(s.a1), b0: bigToHex(s.b0), b1: bigToHex(s.b1) });
204
184
 
205
- // ШАГ 1. ПОДГОТОВКА. Считается ПОСЛЕДОВАТЕЛЬНО и целиком: обязательства зависят от накопленной суммы
206
- // предыдущих, поэтому этот шаг не режется. Здесь же готовятся данные по каждому биту - ровно те, что
207
- // нужны шагу 2, и ничего сверх: сам бит, его обязательства и его случайности. ПОЛОВИНЫ ЗДЕСЬ БОЛЬШЕ НЕТ,
208
- // и это главное свойство: дальше её знать не требуется.
185
+ // STEP 1. PREPARATION. Computed SEQUENTIALLY and whole: commitments depend on the running sum, so this step is
186
+ // not split. Per-bit data is prepared here - exactly what step 2 needs: the bit, its commitments and its
187
+ // randomness. THE HALF IS NO LONGER HERE, and that is the key property.
209
188
  function prepareProof(x, context) {
210
189
  const XA = mul(curves.A, x), XB = mul(curves.B, x);
211
190
  const cA = commitments(curves.A, x), cB = commitments(curves.B, x);
@@ -221,8 +200,8 @@ export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomByt
221
200
  return { XA: enc(XA), XB: enc(XB), context: String(context == null ? "" : context), bits };
222
201
  }
223
202
 
224
- // ШАГ 2. ДОКАЗАТЕЛЬСТВО ОДНОГО БИТА. Вызывается на подготовленных данных, самостоятелен и ничего не
225
- // знает ни о половине, ни о других битах. Из-за этого шаги независимы, а значит и распараллеливаемы.
203
+ // STEP 2. PROVE ONE BIT. Called on the prepared data, self-contained, knowing neither the half nor other bits.
204
+ // So the steps are independent and parallelisable.
226
205
  function proveBit(part, context) {
227
206
  const sig = ringSig(part.bit === 1 ? 1n : 0n, part.i,
228
207
  curves.A.P.fromHex(String(part.cA).replace(/^0x/, "")),
@@ -233,79 +212,75 @@ export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomByt
233
212
  return { i: part.i, cA: part.cA, cB: part.cB, sig: sigOut(sig) };
234
213
  }
235
214
 
236
- // ШАГ 3. СБОРКА. Складывать нечего: порядок частей и есть порядок битов, и он проверяется ниже.
215
+ // STEP 3. ASSEMBLE. Nothing to add: the part order IS the bit order, checked below.
237
216
  function assembleProof(prepared, parts) {
238
217
  const byIndex = new Map(parts.map((p) => [p.i, p]));
239
218
  const proofs = [];
240
219
  for (let i = 0; i < BITS; i++) {
241
220
  const item = byIndex.get(i);
242
- if (!item) throw new Error("assembleProof: нет части для бита " + i);
221
+ if (!item) throw new Error("assembleProof: no part for bit " + i);
243
222
  proofs.push({ cA: item.cA, cB: item.cB, sig: item.sig });
244
223
  }
245
224
  return { XA: prepared.XA, XB: prepared.XB, proofs };
246
225
  }
247
226
 
248
- // prove ОСТАЁТСЯ ТОЙ ЖЕ ФУНКЦИЕЙ и собрана из тех же трёх шагов: второй реализации одного и того же
249
- // здесь не появилось - иначе проверки гоняли бы один код, а работал другой.
227
+ // prove REMAINS THE SAME FUNCTION, assembled from the same three steps: no second implementation appeared.
250
228
  function prove(x, context, onBit) {
251
229
  const prepared = prepareProof(x, context);
252
230
  const parts = [];
253
231
  for (let i = 0; i < BITS; i++) {
254
- // Прогресс по ходу счёта. 253 бита - это те самые десять секунд, и без промежуточных сообщений
255
- // интерфейс не может показать, что работа идёт, а не встала. Сообщаем каждые 16 бит и на последнем.
232
+ // Progress during the count. 253 bits is those ten seconds, and without interim messages the interface
233
+ // cannot show the work is running. Reported every 16 bits and at the last.
256
234
  if (onBit && (i % 16 === 0 || i === BITS - 1)) onBit(i + 1, BITS);
257
235
  parts.push(proveBit(prepared.bits[i], prepared.context));
258
236
  }
259
237
  return assembleProof(prepared, parts);
260
238
  }
261
239
 
262
- // СТРОГАЯ ПРОВЕРКА ПОДГРУППЫ, А НЕ ТОЛЬКО МАЛОГО ПОРЯДКА. Прежняя редакция проверяла [8]*P != 0:
263
- // точку ЧИСТО малого порядка она отвергала, а точку с малой СОСТАВЛЯЮЩЕЙ (P = Q + T, где T порядка 2, 4
264
- // или 8) пропускала - и вырожденное равенство проходило. Строгое условие: [l]*P = 0, где l - порядок
265
- // подгруппы. secp256k1 не проверяется: у неё кофактор 1, и вся группа простого порядка.
240
+ // STRICT SUBGROUP CHECK, NOT JUST SMALL ORDER. The old edition checked [8]*P != 0: it rejected a purely
241
+ // small-order point but let a point with a small COMPONENT through, so a degenerate equality passed. Strict:
242
+ // [l]*P = 0. secp256k1 is not checked: cofactor 1, the whole group is prime order.
266
243
  function torsionFree(curve, point) {
267
244
  if (curve.name !== "ed25519") return true;
268
- // ЧТО ИМЕННО ЗДЕСЬ СЧИТАЕТСЯ. isTorsionFree() у @noble/curves - это ровно [n]*P = 0 (проверено по
269
- // исходнику пакета: wnaf.mulUnsafe(this, CURVE.n).is0()), то есть точка лежит в подгруппе простого
270
- // порядка. Своя попытка записать это через multiplyUnsafe(order) ПАДАЕТ: библиотека требует скаляр
271
- // меньше порядка и на самом порядке бросает - то есть слагаемое проверки выглядело бы как отказ.
245
+ // WHAT IS COUNTED HERE. isTorsionFree() in @noble/curves is exactly [n]*P = 0, i.e. the point is in the
246
+ // prime-order subgroup. Writing it via multiplyUnsafe(order) FAILS: the library requires a scalar below the
247
+ // order, so the check term would look like a refusal.
272
248
  try { return point.isTorsionFree() === true; } catch { return false; }
273
249
  }
274
- // ФОРМА ПРОВЕРЯЕТСЯ ДО СЧЁТА. Прежний цикл шёл по длине присланного списка: короткое доказательство
275
- // доказывало ДРУГОЕ представление, длинное - жгло время проверки (доказательство ~145 КБ и так считается
276
- // секундами). Честный путь всегда шлёт ровно BITS частей. Неразбираемое поле возвращает отказ, а не
277
- // исключение наружу: вызывающий обязан получать {ok:false}, а не падать.
250
+ // FORM IS CHECKED BEFORE COUNTING. The old loop ran over the received list length: a short proof proved
251
+ // ANOTHER representation, a long one burned check time. An honest path always sends exactly BITS parts. An
252
+ // unparsable field returns a refusal, not an exception: the caller must get {ok:false}, not crash.
278
253
  function verify(proof, context) {
279
- if (!proof || typeof proof !== "object") return { ok: false, reason: "доказательство: ожидается объект" };
280
- if (typeof proof.XA !== "string" || typeof proof.XB !== "string") return { ok: false, reason: "доказательство: нет XA или XB" };
281
- if (!Array.isArray(proof.proofs)) return { ok: false, reason: "доказательство: нет списка частей" };
282
- if (proof.proofs.length !== BITS) return { ok: false, reason: "доказательство: частей " + proof.proofs.length + ", а нужно " + BITS };
254
+ if (!proof || typeof proof !== "object") return { ok: false, reason: "proof: an object is expected" };
255
+ if (typeof proof.XA !== "string" || typeof proof.XB !== "string") return { ok: false, reason: "proof: no XA or XB" };
256
+ if (!Array.isArray(proof.proofs)) return { ok: false, reason: "proof: no parts list" };
257
+ if (proof.proofs.length !== BITS) return { ok: false, reason: "proof: " + proof.proofs.length + " parts, expected " + BITS };
283
258
  const P = (curve, h) => curve.P.fromHex(String(h).replace(/^0x/, ""));
284
259
  let XA, XB, csA, csB;
285
260
  try {
286
261
  XA = P(curves.A, proof.XA); XB = P(curves.B, proof.XB);
287
- if (!torsionFree(curves.B, XB)) return { ok: false, reason: "доказательство: XB не лежит в подгруппе простого порядка" };
262
+ if (!torsionFree(curves.B, XB)) return { ok: false, reason: "proof: XB is not in the prime-order subgroup" };
288
263
  } catch (e) {
289
- return { ok: false, reason: "доказательство: XA или XB не разбирается (" + String((e && e.message) || e) + ")" };
264
+ return { ok: false, reason: "proof: XA or XB is unparsable (" + String((e && e.message) || e) + ")" };
290
265
  }
291
- // НОМЕР БИТА НАЗЫВАЕТСЯ И ЗДЕСЬ: без него отказ «где-то в доказательстве» неотличим от порчи любого из
292
- // 253 обязательств, а разбирать 145 КБ руками никто не станет.
266
+ // THE BIT NUMBER IS NAMED HERE TOO: without it, "somewhere in the proof" is indistinguishable from any of
267
+ // the 253 commitments being corrupt.
293
268
  try {
294
269
  csA = proof.proofs.map((item, i) => {
295
- try { return P(curves.A, item.cA); } catch (e) { throw new Error("бит " + i + ": " + String((e && e.message) || e)); }
270
+ try { return P(curves.A, item.cA); } catch (e) { throw new Error("bit " + i + ": " + String((e && e.message) || e)); }
296
271
  });
297
272
  csB = proof.proofs.map((item, i) => {
298
- try { return P(curves.B, item.cB); } catch (e) { throw new Error("бит " + i + ": " + String((e && e.message) || e)); }
273
+ try { return P(curves.B, item.cB); } catch (e) { throw new Error("bit " + i + ": " + String((e && e.message) || e)); }
299
274
  });
300
275
  } catch (e) {
301
- return { ok: false, reason: "доказательство: " + String((e && e.message) || e) };
276
+ return { ok: false, reason: "proof: " + String((e && e.message) || e) };
302
277
  }
303
- if (!sumCommitments(curves.A, csA).equals(XA)) return { ok: false, reason: "сумма обязательств не даёт X на secp256k1" };
304
- if (!sumCommitments(curves.B, csB).equals(XB)) return { ok: false, reason: "сумма обязательств не даёт X на ed25519" };
278
+ if (!sumCommitments(curves.A, csA).equals(XA)) return { ok: false, reason: "the commitment sum does not give X on secp256k1" };
279
+ if (!sumCommitments(curves.B, csB).equals(XB)) return { ok: false, reason: "the commitment sum does not give X on ed25519" };
305
280
  for (let i = 0; i < proof.proofs.length; i++) {
306
281
  const item = proof.proofs[i];
307
282
  try {
308
- if (!torsionFree(curves.B, csB[i])) return { ok: false, reason: "бит " + i + ": обязательство не лежит в подгруппе простого порядка" };
283
+ if (!torsionFree(curves.B, csB[i])) return { ok: false, reason: "bit " + i + ": the commitment is not in the prime-order subgroup" };
309
284
  const cA = item.cA, cB = item.cB;
310
285
  const sig = Object.fromEntries(Object.entries(item.sig).map(([k, v]) => [k, BigInt(String(v).startsWith("0x") ? v : "0x" + v)]));
311
286
  const pA = P(curves.A, cA), pB = P(curves.B, cB);
@@ -318,9 +293,9 @@ export function createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomByt
318
293
  const bH0 = mul(curves.B, sig.b0, curves.B.Galt), ecB = mPublic(curves.B, subP(pB, curves.B.G), eB);
319
294
  const eA0 = challenge(curves.A, pre(subP(aG0, ecA), subP(bH0, ecB)), context);
320
295
  const eB0 = challenge(curves.B, pre(subP(aG0, ecA), subP(bH0, ecB)), context);
321
- if (eA0 !== sig.eA || eB0 !== sig.eB) return { ok: false, reason: "бит " + i + ": вызовы не сошлись" };
296
+ if (eA0 !== sig.eA || eB0 !== sig.eB) return { ok: false, reason: "bit " + i + ": challenges did not match" };
322
297
  } catch (e) {
323
- return { ok: false, reason: "бит " + i + ": поля не разбираются (" + String((e && e.message) || e) + ")" };
298
+ return { ok: false, reason: "bit " + i + ": fields are unparsable (" + String((e && e.message) || e) + ")" };
324
299
  }
325
300
  }
326
301
  return { ok: true };