@ninsei-labs/sdk 0.39.2 → 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.
- package/CHANGELOG.md +63 -0
- package/README.md +4 -1
- package/engine/atomic/dleq2.js +86 -111
- package/engine/atomic/halfenc.js +58 -72
- package/engine/atomic/halves.js +31 -53
- package/engine/atomic/order-client.js +15 -16
- package/engine/atomic/order.js +44 -62
- package/engine/atomic/orderContext.js +26 -50
- package/engine/atomic/orderHalfCommit.js +17 -18
- package/engine/core/chainSource.js +54 -69
- package/engine/core/clock.js +11 -12
- package/engine/core/config.js +554 -449
- package/engine/core/finality.js +34 -53
- package/engine/core/format.js +35 -36
- package/engine/core/preSignGuards.js +109 -117
- package/engine/core/rfqSource.js +57 -75
- package/engine/core/sellWindow.js +23 -24
- package/engine/core/store.js +20 -21
- package/engine/core/swap.js +242 -243
- package/engine/evm/amounts.js +50 -51
- package/engine/evm/asyncExec.js +577 -0
- package/engine/evm/asyncLegs.js +308 -0
- package/engine/evm/asyncRoute.js +179 -0
- package/engine/evm/auth.js +41 -48
- package/engine/evm/claimGas.js +28 -47
- package/engine/evm/depositor.js +39 -43
- package/engine/evm/dex.js +266 -267
- package/engine/evm/escrow.js +113 -129
- package/engine/evm/factory.js +71 -77
- package/engine/evm/fees.js +41 -42
- package/engine/evm/funding.js +160 -162
- package/engine/evm/gasReserve.js +67 -36
- package/engine/evm/index.js +46 -53
- package/engine/evm/injected.js +34 -42
- package/engine/evm/permit.js +129 -56
- package/engine/evm/prices.js +30 -45
- package/engine/evm/session.js +188 -189
- package/engine/mock/chain.js +17 -18
- package/engine/monero/address.js +33 -48
- package/engine/monero/halvesSweep.js +25 -43
- package/engine/monero/node.js +14 -15
- package/engine/monero/scan.js +82 -95
- package/engine/monero/swapKeys.js +30 -33
- package/engine/monero/unlock.js +17 -20
- package/engine/monero/wallet.js +32 -42
- package/engine/recovery/recoveryFile.js +154 -161
- package/engine/recovery/restore.js +50 -61
- package/engine/sdk/bridge.js +244 -234
- package/engine/ui/dom.js +4 -5
- package/engine/ui/walletModal.js +20 -31
- package/index.d.ts +26 -0
- package/package.json +1 -1
- package/src/adapters/evm-wallet.mjs +16 -0
- package/src/adapters/monero-wallet.mjs +1 -1
- package/src/adapters/monero.mjs +1 -1
- package/src/adapters/node-test-evm-wallet.mjs +5 -5
- package/src/codes.mjs +5 -0
- package/src/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- package/src/index.mjs +16 -2
- package/src/legs/cow-spec.mjs +254 -0
- package/src/legs/cow.mjs +315 -0
- package/src/legs/evm.mjs +86 -2
- package/src/legs/kyber-spec.mjs +265 -0
- package/src/legs/kyber.mjs +425 -0
- package/src/legs/shape.mjs +35 -0
- package/src/legs/uniswaprelay-spec.mjs +541 -0
- package/src/legs/uniswaprelay.mjs +422 -0
- package/src/legs/uniswapx-spec.mjs +329 -0
- package/src/legs/uniswapx.mjs +362 -0
- package/src/quotes.mjs +23 -5
- package/src/swaps.mjs +18 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,66 @@
|
|
|
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
|
+
|
|
36
|
+
## 0.39.3 — 2026-10-09
|
|
37
|
+
|
|
38
|
+
A NODE'S ADDRESS DENYLIST REFUSAL REACHES THE APP AS ITS OWN CODE, AND THE INTERFACE CAN CHECK ADDRESSES ITSELF (issue #16).
|
|
39
|
+
A maker's node refuses a firm quote to an address on the maker's denylist (`PUT /v1/denylist`) and answers
|
|
40
|
+
`{ok:false, code:"denied-address", why:"..."}`. Until now `quotes.firm` read the answer with `http.json`, which throws
|
|
41
|
+
`server-unavailable` and LOSES the body on any non-2xx: the app was told the node was unavailable when the node had
|
|
42
|
+
answered and refused, and it could not tell "did not answer" from "answered and refused".
|
|
43
|
+
|
|
44
|
+
- **`quotes.firm` reads the answer as a VALUE** (`http.tryJson`, `src/quotes.mjs`): a failed CONNECTION is still
|
|
45
|
+
`server-unavailable`, while an answer that refused is its own refusal code (`quote-refused`), not "unavailable".
|
|
46
|
+
- **The node's `denied-address` refusal reaches the app under the SDK code `address-denied`** (`src/codes.mjs`,
|
|
47
|
+
`index.d.ts`), WITHOUT the node's `why` text and WITHOUT the address: the node names both, and the interface shows
|
|
48
|
+
its own neutral message. A backend relaying the quote passes the node's `code` through (the demo's
|
|
49
|
+
`app/server.mjs` `/api/order-quote` relay now forwards it).
|
|
50
|
+
- **A new optional address check: `createNinsei({ ..., denylist: [addresses] })`** (`src/index.mjs`, `src/quotes.mjs`,
|
|
51
|
+
`src/swaps.mjs`, `index.d.ts`). Compared WITHOUT CASE; an empty/absent list means "no check"; the SDK fetches
|
|
52
|
+
nothing - the interface brings the list in. Before `quotes.firm` the order's `locker` is checked (on a purchase the
|
|
53
|
+
claimer is the node's own address, so it is deliberately not checked); before `swaps.start` the ACTUAL sender is
|
|
54
|
+
checked - the address the funding transaction will really be sent from, read from the wallet adapter's `address()`
|
|
55
|
+
(`eth_accounts[0]`) at the moment of sending, because the person may have switched accounts. A barred address is
|
|
56
|
+
refused with `address-denied` BEFORE any request; an adapter that cannot hand out the current account is refused
|
|
57
|
+
with `bad-input` rather than skipping the policy silently.
|
|
58
|
+
- Checks (`tools/check-sdk.mjs`): the node's `denied-address` answer yields `address-denied` with neither the address
|
|
59
|
+
nor the `why` in `params`; a plain refusal yields `quote-refused` while a failed connection yields
|
|
60
|
+
`server-unavailable`; a denylisted `order.locker` is refused before any request (zero calls seen); a denylisted
|
|
61
|
+
actual sender stops the lock before it signs (no transaction); the comparison is case-insensitive; an empty list
|
|
62
|
+
changes nothing. The breaking run removes the `denied-address` mapping and the pre-send check and must redden.
|
|
63
|
+
|
|
1
64
|
## Unreleased — 2026-10-08
|
|
2
65
|
|
|
3
66
|
EVERY ORDER'S ESCROW IS A CLONE, AND THE CLIENT NOW CHECKS THE IMPLEMENTATION IT WILL EXECUTE (issue #114).
|
package/README.md
CHANGED
|
@@ -96,7 +96,7 @@ Terminal states (`state.terminal`): `success`, `refunded_eth`, `xmr_returned`, `
|
|
|
96
96
|
### Errors (`error.code`, `SdkError.code`)
|
|
97
97
|
|
|
98
98
|
`not-implemented`, `bad-input`, `wallet-not-connected`, `wallet-rejected`, `wrong-chain`, `quote-unavailable`,
|
|
99
|
-
`quote-refused`, `quote-stale`, `recovery-declined`, `recovery-failed`, `storage-unavailable`,
|
|
99
|
+
`quote-refused`, `address-denied`, `quote-stale`, `recovery-declined`, `recovery-failed`, `storage-unavailable`,
|
|
100
100
|
`server-unavailable`, `server-refused`, `xmr-timeout`, `xmr-locked`, `contract-reverted`, `insufficient-funds`, `unknown`.
|
|
101
101
|
|
|
102
102
|
Three refusals that are easy to merge into one are named DIFFERENTLY, because the advice differs:
|
|
@@ -105,6 +105,9 @@ reason and status are in `params`; investigate); `xmr-timeout` — the wait for
|
|
|
105
105
|
state of the money, not a breakdown); `xmr-locked` — XMR is on the address but the transaction's `unlock_time`
|
|
106
106
|
barred it until a height or a date (`params.until`), so the funds are not an arrival and the ready mark is not
|
|
107
107
|
signed (issue #84).
|
|
108
|
+
`address-denied` (issue #16) - the address is on the interface's `denylist` (refused before any
|
|
109
|
+
request) or the node's own `denied-address` refusal, WITHOUT the node's `why` text and WITHOUT the address: the
|
|
110
|
+
interface shows its own neutral message.
|
|
108
111
|
|
|
109
112
|
## Quotes
|
|
110
113
|
|
package/engine/atomic/dleq2.js
CHANGED
|
@@ -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
|
|
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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
|
26
|
-
//
|
|
27
|
-
if (typeof sha3_512 !== "function") throw new Error("createDleq2:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
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
|
-
//
|
|
57
|
-
//
|
|
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++; //
|
|
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
|
-
//
|
|
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
|
-
//
|
|
91
|
-
//
|
|
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
|
|
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
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
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
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
123
|
-
//
|
|
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
|
-
//
|
|
128
|
-
// multiplyUnsafe
|
|
129
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
255
|
-
//
|
|
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
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
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
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
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
|
-
//
|
|
276
|
-
//
|
|
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: "
|
|
281
|
-
if (!Array.isArray(proof.proofs)) return { ok: false, reason: "
|
|
282
|
-
if (proof.proofs.length !== BITS) return { ok: false, reason: "
|
|
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: "
|
|
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: "
|
|
264
|
+
return { ok: false, reason: "proof: XA or XB is unparsable (" + String((e && e.message) || e) + ")" };
|
|
290
265
|
}
|
|
291
|
-
//
|
|
292
|
-
// 253
|
|
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("
|
|
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("
|
|
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: "
|
|
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: "
|
|
304
|
-
if (!sumCommitments(curves.B, csB).equals(XB)) return { ok: false, reason: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
298
|
+
return { ok: false, reason: "bit " + i + ": fields are unparsable (" + String((e && e.message) || e) + ")" };
|
|
324
299
|
}
|
|
325
300
|
}
|
|
326
301
|
return { ok: true };
|