@ninsei-labs/sdk 0.39.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/CHANGELOG.md +866 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/engine/atomic/dleq2.js +330 -0
- package/engine/atomic/halfenc.js +205 -0
- package/engine/atomic/halves.js +119 -0
- package/engine/atomic/order-client.js +57 -0
- package/engine/atomic/order.js +132 -0
- package/engine/atomic/orderContext.js +70 -0
- package/engine/atomic/orderHalfCommit.js +29 -0
- package/engine/core/chainSource.js +306 -0
- package/engine/core/clock.js +40 -0
- package/engine/core/config.js +885 -0
- package/engine/core/finality.js +93 -0
- package/engine/core/format.js +168 -0
- package/engine/core/preSignGuards.js +495 -0
- package/engine/core/rfqSource.js +193 -0
- package/engine/core/sellWindow.js +43 -0
- package/engine/core/store.js +100 -0
- package/engine/core/swap.js +1153 -0
- package/engine/evm/amounts.js +131 -0
- package/engine/evm/auth.js +156 -0
- package/engine/evm/claimGas.js +87 -0
- package/engine/evm/depositor.js +127 -0
- package/engine/evm/dex.js +836 -0
- package/engine/evm/escrow.js +208 -0
- package/engine/evm/factory.js +149 -0
- package/engine/evm/fees.js +90 -0
- package/engine/evm/funding.js +309 -0
- package/engine/evm/gasReserve.js +88 -0
- package/engine/evm/index.js +123 -0
- package/engine/evm/injected.js +175 -0
- package/engine/evm/permit.js +125 -0
- package/engine/evm/prices.js +179 -0
- package/engine/evm/session.js +777 -0
- package/engine/mock/chain.js +98 -0
- package/engine/monero/address.js +265 -0
- package/engine/monero/halvesSweep.js +68 -0
- package/engine/monero/node.js +110 -0
- package/engine/monero/scan.js +325 -0
- package/engine/monero/swapKeys.js +69 -0
- package/engine/monero/unlock.js +46 -0
- package/engine/monero/wallet.js +168 -0
- package/engine/recovery/recoveryFile.js +363 -0
- package/engine/recovery/restore.js +176 -0
- package/engine/sdk/bridge.js +609 -0
- package/engine/ui/dom.js +90 -0
- package/engine/ui/walletModal.js +132 -0
- package/index.d.ts +1417 -0
- package/package.json +74 -0
- package/src/actions.mjs +72 -0
- package/src/adapters/evm-wallet.mjs +126 -0
- package/src/adapters/index.mjs +21 -0
- package/src/adapters/local-storage.mjs +32 -0
- package/src/adapters/monero-wallet.mjs +185 -0
- package/src/adapters/monero.mjs +51 -0
- package/src/adapters/node-test-evm-wallet.mjs +345 -0
- package/src/codes.mjs +188 -0
- package/src/config.mjs +277 -0
- package/src/engine.mjs +53 -0
- package/src/errors.mjs +24 -0
- package/src/http.mjs +72 -0
- package/src/index.mjs +256 -0
- package/src/legs/evm.mjs +118 -0
- package/src/legs/index.mjs +50 -0
- package/src/levelsWalkSpec.mjs +183 -0
- package/src/lock.mjs +140 -0
- package/src/order.mjs +212 -0
- package/src/preflight.mjs +137 -0
- package/src/primitives.mjs +47 -0
- package/src/quoteEip712.mjs +35 -0
- package/src/quoteEip712Spec.mjs +214 -0
- package/src/quotes.mjs +465 -0
- package/src/record.mjs +65 -0
- package/src/recovery.mjs +40 -0
- package/src/sim.mjs +37 -0
- package/src/swap-flow.mjs +492 -0
- package/src/swaps.mjs +935 -0
- package/src/sweep.mjs +222 -0
- package/src/wallet.mjs +164 -0
- package/src/xmr-arrival.mjs +289 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ninsei Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @ninsei-labs/sdk — the XMR ↔ EVM swap engine
|
|
2
|
+
|
|
3
|
+
The package is responsible for keys, proofs, halves, the recovery file, quotes, guards, contracts and talking to
|
|
4
|
+
the service. The interface is responsible for screens, layout and texts. The boundary is drawn by code, not by
|
|
5
|
+
agreement: there is nothing to lay out here, and nothing to compute there.
|
|
6
|
+
|
|
7
|
+
The package **knows nothing about the DOM**: neither `document`, nor `window`, nor `localStorage` (checked by
|
|
8
|
+
machine). The wallet arrives as an EIP-1193 provider, storage as an adapter, and the interface opens the windows.
|
|
9
|
+
|
|
10
|
+
## State (0.1.0)
|
|
11
|
+
|
|
12
|
+
| Surface | State |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `config` — networks, tokens, limits, codes | works |
|
|
15
|
+
| `wallet` — EIP-1193 from outside, address, network, network switching, subscription | works |
|
|
16
|
+
| `preflight` — the engine's guards, answering with codes | works |
|
|
17
|
+
| `recovery.open` — parsing the recovery file | works |
|
|
18
|
+
| `sim` — sandbox scenarios and time | works (applying to a swap — stage 2) |
|
|
19
|
+
| `quotes.watch` | works: live indicative quotes, its own client; a snapshot carries the display fields (sides, networks, `why`, signature, age, maturity), and the size can be NAMED (`size`) |
|
|
20
|
+
| `quotes.firm` | answers `not-implemented` (stage 2: signing with a key and the order protocol) |
|
|
21
|
+
| `swaps.list` / `swaps.get` | read state from storage (and after a page reload) |
|
|
22
|
+
| `swaps.start` | holds the order: guards → recovery file and the person's confirmation → order → locking funds → **recording with us and watching the Monero address**; returns the named state `step: "xmr-incoming"` |
|
|
23
|
+
| `swaps.watch` | waits for XMR to arrive at the joint address with the same request as the page (`GET /swaps/{id}`): arrived — returns the fact (amount, confirmations, txid); did not wait — a refusal with the code `xmr-timeout`; already on the address but barred by `unlock_time` — a refusal with the code `xmr-locked` (the boundary is in `params.until`), and NO ready mark; with expectations and a wallet, places the ready mark after the arrival |
|
|
24
|
+
| `wallet.balances` | works: the native coin and tokens, in minimal units |
|
|
25
|
+
|
|
26
|
+
What is not ready names itself with the code `not-implemented` with the fields `surface` and `stage`: an empty
|
|
27
|
+
value that looks like data is more dangerous than a refusal.
|
|
28
|
+
|
|
29
|
+
## The contract
|
|
30
|
+
|
|
31
|
+
`index.d.ts` is the single source of truth about the API shape, and this is checked: `tools/check-sdk.mjs`
|
|
32
|
+
compares the list of what the SDK returns with what is declared in the contract (both ways).
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import { createNinsei } from "@ninsei-labs/sdk";
|
|
36
|
+
|
|
37
|
+
const sdk = createNinsei({
|
|
38
|
+
settlement: { chain: "arbitrum-sepolia" }, // from sdk.config.chains; vm is taken from the registry
|
|
39
|
+
xmrNetwork: "stagenet",
|
|
40
|
+
apiBase: "/api",
|
|
41
|
+
assetsBase: "/sdk/",
|
|
42
|
+
mode: "live", // or "sim"
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
sdk.config.evmNetwork.tokens // the token list: the interface does not hardcode it
|
|
46
|
+
import { evmWallet } from "@ninsei-labs/sdk/adapters";
|
|
47
|
+
sdk.wallet.use(evmWallet(eip1193Provider)); // the adapter is brought by the interface, the SDK opens no windows
|
|
48
|
+
await sdk.wallet.connect({ chainId: sdk.config.settlement.chain.chainId });
|
|
49
|
+
const verdict = await sdk.preflight(quote, { payBalanceWei, payAmountWei });
|
|
50
|
+
if (!verdict.ok) render(verdict.blockedBy); // codes, not phrases
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Code tables
|
|
54
|
+
|
|
55
|
+
It is the interface that must show the person anything based on these codes: the SDK has not a single
|
|
56
|
+
user-facing string.
|
|
57
|
+
|
|
58
|
+
### Steps (`state.step`)
|
|
59
|
+
|
|
60
|
+
| Code | What it means |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `awaiting_funding` | nothing has arrived at the Monero address yet |
|
|
63
|
+
| `funded` | ETH is in escrow, waiting for XMR |
|
|
64
|
+
| `maker_locking` | the maker is depositing XMR |
|
|
65
|
+
| `xmr_locked` | XMR is at the address, confirmations are coming in |
|
|
66
|
+
| `ready` | the "checked by you" mark has been placed |
|
|
67
|
+
| `claimed` | the maker took the ETH |
|
|
68
|
+
| `swept` | the XMR has been sent to the recipient's address |
|
|
69
|
+
|
|
70
|
+
Terminal states (`state.terminal`): `success`, `refunded_eth`, `xmr_returned`, `closed`.
|
|
71
|
+
|
|
72
|
+
### Checks (`preflight().checks[].code`)
|
|
73
|
+
|
|
74
|
+
| Code | What is checked | From which guard kinds |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `balance` | is there enough to pay | `balance-enough`, `balance-short`, `balance-unchecked` |
|
|
77
|
+
| `gas` | is anything left for gas | `gas-enough`, `gas-short`, `reserve-unstated`, `native-unchecked` |
|
|
78
|
+
| `allowance` | the token allowance | `allowed`, `not-needed`, `allowance-short`, `allowance-unchecked` |
|
|
79
|
+
| `consent` | the person's consent to the allowance | `consent-missing` |
|
|
80
|
+
| `network` | the wallet's network against the settlement network | `network-match`, `wallet-network`, `wallet-unknown`, `network-unknown` |
|
|
81
|
+
| `settlement-network` | on which settlement network the swap is counted | `settlement-network` |
|
|
82
|
+
| `stale` | is the quote's deadline alive | `fresh`, `stale`, `no-ttl`, `untimed`, `expired` |
|
|
83
|
+
| `deadline` | the mark and claim windows from the chain | `deadlines-ok`, `deadlines-unstated`, `ready-by-in-past`, `t1-not-after-ready-by`, `ready-window-too-short`, `claim-window-too-short` |
|
|
84
|
+
| `range` | the size within the node's range | `in-range`, `above-max`, `below-min`, `range-unstated`, `unit-mismatch` |
|
|
85
|
+
| `size` | is the size named at all | `size-unstated`, `amount-unstated` |
|
|
86
|
+
| `escrow-state` | the escrow state on the chain | `new`, `already-funded`, `already-claimed`, `already-refunded`, `state-unchecked`, `off-step` |
|
|
87
|
+
| `escrow-points` | points and halves against the chain | `points-match`, `point-mismatch`, `commit-mismatch`, `points-unchecked` |
|
|
88
|
+
| `counterparty` | the counterparty is real, not a stand-in | `real-provider`, `standin-on-production` |
|
|
89
|
+
| `slots` | slots in the order book | `slots-match`, `slots-unchecked` |
|
|
90
|
+
| `unchecked` | could not be computed | `unchecked` |
|
|
91
|
+
| `unknown` | a kind not in the table | — |
|
|
92
|
+
|
|
93
|
+
`checked: false` means "not measured", not "good": the interface must show this separately from green.
|
|
94
|
+
`params` carries the numbers the verdict was taken from (`need`, `have`, `symbol`, `min`, `max`, `expiresAt`, ...).
|
|
95
|
+
|
|
96
|
+
### Errors (`error.code`, `SdkError.code`)
|
|
97
|
+
|
|
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`,
|
|
100
|
+
`server-unavailable`, `server-refused`, `xmr-timeout`, `xmr-locked`, `contract-reverted`, `insufficient-funds`, `unknown`.
|
|
101
|
+
|
|
102
|
+
Three refusals that are easy to merge into one are named DIFFERENTLY, because the advice differs:
|
|
103
|
+
`server-unavailable` — the service did not answer (retry); `server-refused` — it answered and did not accept (its
|
|
104
|
+
reason and status are in `params`; investigate); `xmr-timeout` — the wait for the XMR to arrive expired (this is a
|
|
105
|
+
state of the money, not a breakdown); `xmr-locked` — XMR is on the address but the transaction's `unlock_time`
|
|
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
|
+
signed (issue #84).
|
|
108
|
+
|
|
109
|
+
## Quotes
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
const stop = sdk.quotes.watch({ direction: "buy", token: "eth", amount: 1 }, (s) => render(s));
|
|
113
|
+
s.rate; // the best offer's price: non-XMR coins per 1 XMR; null - there is no offer (this is not zero)
|
|
114
|
+
s.xmr; // how much XMR for the entered amount (buy) or how much the person gives (sell)
|
|
115
|
+
s.sizeOk; // does the size fall within the node's grid and range
|
|
116
|
+
s.sizeCode; // why it does not: below-min | above-max | bad-size | no-offer
|
|
117
|
+
s.offers; // all offers for the size; s.refused - rows that did not become offers
|
|
118
|
+
s.best; // the best for the requested volume and side, AS THE SERVICE NAMED IT (pair and node)
|
|
119
|
+
stop(); // the last one to unsubscribe stops the polling
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The size can be NAMED rather than derived: `watch({ direction, token, size }, cb)` — if the caller has already
|
|
123
|
+
aligned the volume to the provider's grid, the core asks for EXACTLY that size. An offer row and a refusal row
|
|
124
|
+
carry the pair's sides, both networks and the "named" marker, the provider's reason (`why`), the package's
|
|
125
|
+
signature, the age (`ageMs`) and the maturity level (`finality`) — display fields, not a second computation.
|
|
126
|
+
|
|
127
|
+
The price does not depend on volume: the volume is needed by the provider to say whether it takes such a size. So
|
|
128
|
+
the size is computed from the person's amount and is snapped to the node's grid **down**, and a size outside the
|
|
129
|
+
range is not aligned: it is the provider's refusal, and it arrives as a code. Indicative quotes commit to nothing;
|
|
130
|
+
`quotes.firm` commits (stage 2).
|
|
131
|
+
|
|
132
|
+
## Extending to other networks (Tron, Solana and beyond)
|
|
133
|
+
|
|
134
|
+
The package name is deliberately common: the shared part — the quote client, the Monero leg, the state machine,
|
|
135
|
+
recovery, the codes — stays the same for any settlement chain. The extension is done through three seams, and none
|
|
136
|
+
of them requires edits in the core:
|
|
137
|
+
|
|
138
|
+
| Seam | What it is | What a new network adds |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `sdk/src/legs/` | the leg driver registry: `vm → driver` | a driver file (`vm`, `walletChainId`, `sameChain`, then — deposit/mark/claim/refund/read state) and one registration line |
|
|
141
|
+
| `sdk/src/adapters/` | wallet and storage | `tronWallet(...)`, `solanaWallet(...)` next to `evmWallet(...)` |
|
|
142
|
+
| the contract | `settlement.vm`, `counterparty.{chain,vm,leg}`, the check codes | nothing: the names already name a role, not a chain |
|
|
143
|
+
|
|
144
|
+
What is already ready for this now: a network without a driver answers `not-implemented` and **names** the vm,
|
|
145
|
+
rather than being assumed EVM by default; an adapter must be for the same vm as the settlement network; the driver
|
|
146
|
+
registry is checked for completeness (a half-added driver reddens the check).
|
|
147
|
+
|
|
148
|
+
Separately, about what is already in place:
|
|
149
|
+
|
|
150
|
+
- **The authorization method** is set by the adapter, not the core: `evmWallet(provider)` accepts any EIP-1193
|
|
151
|
+
provider (injected, WalletConnect, your own broker), and for another VM its own adapter will stand next to it.
|
|
152
|
+
The core needs only `connect`, `address`, `chainId`, `subscribe` and `driver()`. If a signer without a provider
|
|
153
|
+
appears (a local key, a hardware wallet, MPC), the adapter will gain a "sign" capability — that is an edit of one
|
|
154
|
+
interface, not a rewrite of the swap.
|
|
155
|
+
- **Executing the DEX leg and aggregators.** The route-provider registry already exists in the EVM driver:
|
|
156
|
+
`routeProviders()` and `routeProviderFor(id)`, a requirement for a provider — `id`, `kind`, `shape`, `plan(...)` and, for `shape: "async"`, `settled(...)`.
|
|
157
|
+
One provider is declared: `declared` (our routes; execution — stage 3). An external aggregator (1inch and the
|
|
158
|
+
like) is added **by a file and a line in the registry**, and the order of preference is set by options
|
|
159
|
+
(`routing: { prefer: [...] }`) and checked against the registry: an unknown identifier is `bad-input`, not
|
|
160
|
+
"we'll try and see". The pre-signature guards (the price is not market, the amount is covered, the token
|
|
161
|
+
allowance) stay shared by all providers: otherwise an external route would become a hole in those very guards.
|
|
162
|
+
|
|
163
|
+
Bitcoin is a separate case and, most likely, a separate SDK: it has no contract in the EVM-contract sense, the leg
|
|
164
|
+
is done by a script (HTLC or a signature adapter), and the wallet needs PSBT rather than a provider. The shared
|
|
165
|
+
core is reused in that case: quotes, the Monero leg, states and recovery do not depend on the settlement chain.
|
|
166
|
+
|
|
167
|
+
The check codes tied to EVM are named plainly: `allowance` (token allowances) and `gas` (the network-fee margin)
|
|
168
|
+
trigger where those exist; on another VM they simply do not appear, rather than turning into "all good".
|
|
169
|
+
|
|
170
|
+
## Rules
|
|
171
|
+
|
|
172
|
+
1. **No DOM.** There is exactly one exception, and it is named: the `src/adapters` directory (the `localStorage`
|
|
173
|
+
adapter is wired in explicitly, `@ninsei-labs/sdk/adapters`). The core loads in bare Node — this is checked.
|
|
174
|
+
2. **No texts.** Not a single user-facing phrase: only codes and params.
|
|
175
|
+
Network -> virtual machine -> driver: the SDK takes networks from the engine's registry (`CHAINS`) rather than
|
|
176
|
+
keeping its own list; a network without a driver is rejected by name (`not-implemented` with `vm` and the list
|
|
177
|
+
of supported ones), rather than being assumed EVM.
|
|
178
|
+
Storage arrives through the `storage` option and becomes the core's storage: the state of unfinished swaps and
|
|
179
|
+
the agent session live where the interface decided. Without the option the core takes its default — web storage
|
|
180
|
+
if it is present in the environment, otherwise process memory.
|
|
181
|
+
3. **The wallet is outside.** The SDK opens no windows and does not look for injected providers itself.
|
|
182
|
+
4. **The step order and the guards are inside `swaps.start`** (stage 2): funds are not locked until the interface
|
|
183
|
+
has confirmed the recovery file is saved; retrying after an error or a reload is safe.
|
|
184
|
+
5. **Workers and WASM are separate files** from `assetsBase`, so the interface's bundler (Astro/Vite) does not
|
|
185
|
+
touch them: `atomic/order-worker.js`, the Monero bundles.
|
|
186
|
+
6. **Strict CSP.** The package is designed for a policy without `eval` and inline.
|
|
187
|
+
7. **Zero dependencies.**
|
|
@@ -0,0 +1,330 @@
|
|
|
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.
|
|
4
|
+
// Everything below this header is identical to the source.
|
|
5
|
+
|
|
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.
|
|
9
|
+
//
|
|
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
|
+
//
|
|
18
|
+
// СТРОГАЯ ПРОВЕРКА ПОДГРУППЫ. У ed25519 кофактор 8, и точка может нести составляющую малого порядка:
|
|
19
|
+
// проверка «умножение на 8 не даёт ноль» её пропускает, а именно она делает равенства вырожденными.
|
|
20
|
+
// Проверяем строго: [l]*P = 0, то есть точка лежит в подгруппе простого порядка.
|
|
21
|
+
//
|
|
22
|
+
// ВАЖНО: примитивы аудированы (@noble/curves), ЭТА реализация - нет.
|
|
23
|
+
|
|
24
|
+
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)");
|
|
28
|
+
const ED_ORDER = ed25519.Point.Fn.ORDER, SEC_ORDER = secp256k1.Point.Fn.ORDER;
|
|
29
|
+
// Секрет живёт числом бит МЕНЬШЕГО порядка: так снимается ограничение ks < min(l, n).
|
|
30
|
+
const BITS = Math.min(ED_ORDER.toString(2).length, SEC_ORDER.toString(2).length);
|
|
31
|
+
|
|
32
|
+
const curves = {
|
|
33
|
+
A: {
|
|
34
|
+
name: "secp256k1", P: secp256k1.Point, order: SEC_ORDER,
|
|
35
|
+
G: secp256k1.Point.BASE,
|
|
36
|
+
Galt: secp256k1.Point.fromHex("0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"),
|
|
37
|
+
},
|
|
38
|
+
B: {
|
|
39
|
+
name: "ed25519", P: ed25519.Point, order: ED_ORDER,
|
|
40
|
+
G: ed25519.Point.BASE,
|
|
41
|
+
Galt: ed25519.Point.fromHex("8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94"),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
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
|
+
// умножении. Поэтому импорт модуля не дорожает, а платит первый расчёт - и платит один раз.
|
|
53
|
+
curves.A.Galt.precompute(8);
|
|
54
|
+
curves.B.Galt.precompute(8);
|
|
55
|
+
|
|
56
|
+
// КОНТЕКСТ ИДЁТ СЫРЫМИ БАЙТАМИ UTF-8. Прежний ctxHex собирал hex из charCodeAt и отдавал его в hexBytes:
|
|
57
|
+
// для ASCII это ровно те же байты, но для любого символа выше 0x7F hex выходил длиннее двух цифр, hexBytes
|
|
58
|
+
// резал его по чётности, и доказывающий с проверяющим могли посчитать РАЗНЫЕ вызовы. Канонический контекст
|
|
59
|
+
// ордера - ASCII, поэтому живые котировки не меняются: это проверено сравнением байт (tools/check-dleq2.mjs).
|
|
60
|
+
// Кодировщик свой, а не TextEncoder: он не требует глобального объекта в среде воркера страницы.
|
|
61
|
+
const ctxUtf8 = (context) => {
|
|
62
|
+
const t = String(context == null ? "" : context);
|
|
63
|
+
const out = [];
|
|
64
|
+
for (let i = 0; i < t.length; i++) {
|
|
65
|
+
let c = t.codePointAt(i);
|
|
66
|
+
if (c > 0xffff) i++; // суррогатная пара: codePointAt уже вернул её целиком
|
|
67
|
+
if (c < 0x80) out.push(c);
|
|
68
|
+
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
|
|
69
|
+
else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
70
|
+
else out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
71
|
+
}
|
|
72
|
+
return new Uint8Array(out);
|
|
73
|
+
};
|
|
74
|
+
const enc = (x) => (typeof x === "string" ? x : x.toHex ? x.toHex() : String(x));
|
|
75
|
+
function hexBytes(h) {
|
|
76
|
+
const s = h.replace(/^0x/, "");
|
|
77
|
+
const out = new Uint8Array(s.length / 2);
|
|
78
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
const bigToHex = (v) => v.toString(16).padStart(64, "0");
|
|
82
|
+
function bytesToBig(b) { let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v; }
|
|
83
|
+
// ЧТЕНИЕ МЛАДШИМ БАЙТОМ ВПЕРЁД - как у edwards25519 в эталоне. Это не стилистика: с обратным порядком
|
|
84
|
+
// байт вызов получился бы другим, и доказательство не сошлось бы с реализацией на другой стороне.
|
|
85
|
+
function bytesToBigLE(b) { let v = 0n; for (let i = b.length - 1; i >= 0; i--) v = (v << 8n) | BigInt(b[i]); return v; }
|
|
86
|
+
function concat(parts) {
|
|
87
|
+
const t = parts.reduce((s, p) => s + p.length, 0), out = new Uint8Array(t);
|
|
88
|
+
let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } return out;
|
|
89
|
+
}
|
|
90
|
+
// Вызов: один и тот же preimage, но приведение к скаляру - по правилам СВОЕЙ кривой.
|
|
91
|
+
// ПРИВЯЗКА К ОРДЕРУ. Контекст (например, адрес эскроу и hashlock) входит в preimage вызова.
|
|
92
|
+
// Без него доказательство можно было бы переиспользовать в другом ордере с той же половиной:
|
|
93
|
+
// в вызове участвовали только точки и обязательства. Найдено при сверке документов, раздел 21.2.
|
|
94
|
+
// Контекст ордера НЕ здесь: он уже входит в parts через сборку preimage (см. ctxUtf8). Одна механика
|
|
95
|
+
// на обе стороны - иначе доказывающий и проверяющий считали бы вызов по-разному (так и вышло в первом
|
|
96
|
+
// варианте правки: параметр был добавлен в сигнатуру, а подставлен в один вызов из двенадцати).
|
|
97
|
+
function challenge(curve, parts) {
|
|
98
|
+
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");
|
|
100
|
+
return bytesToBigLE(h) % curve.order;
|
|
101
|
+
}
|
|
102
|
+
// СЛУЧАЙНЫЙ СКАЛЯР - ТОЖЕ ШИРОКОЕ ПРИВЕДЕНИЕ: 64 случайных байта по модулю порядка, как NewRandomScalar
|
|
103
|
+
// в эталоне. Прежний цикл брал 32 байта и отбраковывал всё, что не легло в порядок: смещение около
|
|
104
|
+
// 2^-128 и лишние повторы. Ноль теоретически возможен (порядка 2^-252) и здесь заменяется единицей:
|
|
105
|
+
// для обязательств это значит лишь ненулевую случайность, а не отказ.
|
|
106
|
+
function randScalar(curve) {
|
|
107
|
+
const v = bytesToBigLE(randomBytes(64)) % curve.order;
|
|
108
|
+
return v === 0n ? 1n : v;
|
|
109
|
+
}
|
|
110
|
+
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 секунд.
|
|
115
|
+
//
|
|
116
|
+
// НОЛЬ ЗДЕСЬ ЗАКОНЕН. Это первое, что нужно знать об этой функции: нулевой бит даёт b*G при b=0, и
|
|
117
|
+
// приведённый к нулю вызов тоже встречается. Прежний код возвращал в этом случае нейтральный элемент,
|
|
118
|
+
// и это правильное поведение. Я однажды заменил его проверкой, которая бросала исключение, - и получил
|
|
119
|
+
// падение на ровном месте, потому что комментарий прежнего кода не прочитал, а только переписал.
|
|
120
|
+
// Ноль возвращает нейтральный элемент кривой, multiplyUnsafe(0) не вызывается вовсе: он бросает.
|
|
121
|
+
function mPublic(curve, point, scalar) {
|
|
122
|
+
// Скаляр приходит и числом, и hex-строкой: в доказательстве скаляры хранятся hex, чтобы переживать
|
|
123
|
+
// JSON. Обе формы приводятся здесь, в одном месте.
|
|
124
|
+
const raw = typeof scalar === "bigint" ? scalar : BigInt(scalar);
|
|
125
|
+
const s = ((raw % curve.order) + curve.order) % curve.order;
|
|
126
|
+
if (s === 0n) return curve.P.ZERO;
|
|
127
|
+
// ТОЧКА С ТАБЛИЦЕЙ УМНОЖАЕТСЯ ЧЕРЕЗ multiply, остальные - через multiplyUnsafe. Это не взаимозаменяемо:
|
|
128
|
+
// multiplyUnsafe таблицу не использует и на второй базовой точке остаётся втрое-вшестеро дороже, а
|
|
129
|
+
// multiply БЕЗ таблицы, наоборот, дороже multiplyUnsafe (измерено 7,1 мс против 2,8). Поэтому выбор
|
|
130
|
+
// привязан к конкретной точке, а не к удобству вызова.
|
|
131
|
+
if (point === curve.Galt) return point.multiply(s);
|
|
132
|
+
return point.multiplyUnsafe(s);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Точка из скаляра на второй базовой точке кривой.
|
|
136
|
+
function mul(curve, s, base) {
|
|
137
|
+
return mPublic(curve, base || curve.G, typeof s === "bigint" ? s : BigInt(s));
|
|
138
|
+
}
|
|
139
|
+
const addP = (a, b) => a.add(b);
|
|
140
|
+
const subP = (a, b) => a.subtract(b);
|
|
141
|
+
const encP = (p) => hexBytes(p.toHex());
|
|
142
|
+
const encS = (s) => hexBytes(bigToHex(s));
|
|
143
|
+
|
|
144
|
+
// Обязательства по битам: C_i = b_i*G + r_i*G' , причём Σ r_i*2^i = 0, поэтому Σ C_i*2^i = x*G.
|
|
145
|
+
function commitments(curve, x) {
|
|
146
|
+
const rs = [], cs = [];
|
|
147
|
+
let sum = 0n, pow = 1n;
|
|
148
|
+
for (let i = 0; i < BITS; i++) {
|
|
149
|
+
if (i === BITS - 1) {
|
|
150
|
+
const inv = modInv(pow, curve.order);
|
|
151
|
+
rs[i] = ((curve.order - (sum % curve.order)) * inv) % curve.order;
|
|
152
|
+
} else {
|
|
153
|
+
rs[i] = randScalar(curve);
|
|
154
|
+
sum = (sum + rs[i] * pow) % curve.order;
|
|
155
|
+
pow = (pow * 2n) % curve.order;
|
|
156
|
+
}
|
|
157
|
+
const b = bit(x, i);
|
|
158
|
+
cs.push(addP(mul(curve, b), mul(curve, rs[i], curve.Galt)));
|
|
159
|
+
}
|
|
160
|
+
return { rs, cs };
|
|
161
|
+
}
|
|
162
|
+
function modInv(a, m) {
|
|
163
|
+
let [old_r, r] = [a % m, m], [old_s, s] = [1n, 0n];
|
|
164
|
+
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: не обратим");
|
|
166
|
+
return ((old_s % m) + m) % m;
|
|
167
|
+
}
|
|
168
|
+
function sumCommitments(curve, cs) {
|
|
169
|
+
let sum = cs[0];
|
|
170
|
+
for (let i = 1; i < cs.length; i++) sum = addP(sum, mul(curve, 2n ** BigInt(i) % curve.order, cs[i]));
|
|
171
|
+
return sum;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// OR-доказательство «обязательство к 0 или к 1» для одного бита, по обеим кривым сразу.
|
|
175
|
+
//
|
|
176
|
+
// ПРИНИМАЕТ БИТ, А НЕ ПОЛОВИНУ, и это не украшательство: биту нужен ровно один бит половины и его
|
|
177
|
+
// случайности. Из-за этого шаг САМОДОСТАТОЧЕН - ни половины, ни данных других битов он не требует, а
|
|
178
|
+
// значит именно его можно отдать другому потоку, не передавая туда ключ целиком.
|
|
179
|
+
function ringSig(bitValue, i, cA, cB, rA, rB, context) {
|
|
180
|
+
const j = randScalar(curves.A), k = randScalar(curves.B);
|
|
181
|
+
const pre = (p1, p2) => [ctxUtf8(context), encP(cA), encP(cB), encP(p1), encP(p2)];
|
|
182
|
+
const eA = challenge(curves.A, pre(mul(curves.A, j, curves.A.Galt), mul(curves.B, k, curves.B.Galt)), context);
|
|
183
|
+
const eB = challenge(curves.B, pre(mul(curves.A, j, curves.A.Galt), mul(curves.B, k, curves.B.Galt)), context);
|
|
184
|
+
const b = bitValue === 1n || bitValue === 1 ? 1n : 0n;
|
|
185
|
+
if (b === 0n) {
|
|
186
|
+
const a0 = randScalar(curves.A), b0 = randScalar(curves.B);
|
|
187
|
+
const cAm1 = subP(cA, curves.A.G), cBm1 = subP(cB, curves.B.G);
|
|
188
|
+
const eA0 = challenge(curves.A, pre(subP(mul(curves.A, a0, curves.A.Galt), mPublic(curves.A, cAm1, eA)),
|
|
189
|
+
subP(mul(curves.B, b0, curves.B.Galt), mPublic(curves.B, cBm1, eB))), context);
|
|
190
|
+
const eB0 = challenge(curves.B, pre(subP(mul(curves.A, a0, curves.A.Galt), mPublic(curves.A, cAm1, eA)),
|
|
191
|
+
subP(mul(curves.B, b0, curves.B.Galt), mPublic(curves.B, cBm1, eB))), context);
|
|
192
|
+
return { eA: eA0, eB: eB0, a0, a1: (j + eA0 * rA) % curves.A.order, b0, b1: (k + eB0 * rB) % curves.B.order };
|
|
193
|
+
}
|
|
194
|
+
const a1 = randScalar(curves.A), b1 = randScalar(curves.B);
|
|
195
|
+
const eA1 = challenge(curves.A, pre(subP(mul(curves.A, a1, curves.A.Galt), mPublic(curves.A, cA, eA)),
|
|
196
|
+
subP(mul(curves.B, b1, curves.B.Galt), mPublic(curves.B, cB, eB))), context);
|
|
197
|
+
const eB1 = challenge(curves.B, pre(subP(mul(curves.A, a1, curves.A.Galt), mPublic(curves.A, cA, eA)),
|
|
198
|
+
subP(mul(curves.B, b1, curves.B.Galt), mPublic(curves.B, cB, eB))), context);
|
|
199
|
+
return { eA, eB, a0: (j + eA1 * rA) % curves.A.order, a1, b0: (k + eB1 * rB) % curves.B.order, b1 };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Скаляры уходят в hex: доказательство должно переживать JSON и границу потока - иначе его не передать.
|
|
203
|
+
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
|
+
|
|
205
|
+
// ШАГ 1. ПОДГОТОВКА. Считается ПОСЛЕДОВАТЕЛЬНО и целиком: обязательства зависят от накопленной суммы
|
|
206
|
+
// предыдущих, поэтому этот шаг не режется. Здесь же готовятся данные по каждому биту - ровно те, что
|
|
207
|
+
// нужны шагу 2, и ничего сверх: сам бит, его обязательства и его случайности. ПОЛОВИНЫ ЗДЕСЬ БОЛЬШЕ НЕТ,
|
|
208
|
+
// и это главное свойство: дальше её знать не требуется.
|
|
209
|
+
function prepareProof(x, context) {
|
|
210
|
+
const XA = mul(curves.A, x), XB = mul(curves.B, x);
|
|
211
|
+
const cA = commitments(curves.A, x), cB = commitments(curves.B, x);
|
|
212
|
+
const bits = [];
|
|
213
|
+
for (let i = 0; i < BITS; i++) {
|
|
214
|
+
bits.push({
|
|
215
|
+
i,
|
|
216
|
+
bit: bit(x, i) === 1n ? 1 : 0,
|
|
217
|
+
cA: enc(cA.cs[i]), cB: enc(cB.cs[i]),
|
|
218
|
+
rA: bigToHex(cA.rs[i]), rB: bigToHex(cB.rs[i]),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return { XA: enc(XA), XB: enc(XB), context: String(context == null ? "" : context), bits };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ШАГ 2. ДОКАЗАТЕЛЬСТВО ОДНОГО БИТА. Вызывается на подготовленных данных, самостоятелен и ничего не
|
|
225
|
+
// знает ни о половине, ни о других битах. Из-за этого шаги независимы, а значит и распараллеливаемы.
|
|
226
|
+
function proveBit(part, context) {
|
|
227
|
+
const sig = ringSig(part.bit === 1 ? 1n : 0n, part.i,
|
|
228
|
+
curves.A.P.fromHex(String(part.cA).replace(/^0x/, "")),
|
|
229
|
+
curves.B.P.fromHex(String(part.cB).replace(/^0x/, "")),
|
|
230
|
+
BigInt("0x" + String(part.rA).replace(/^0x/, "")),
|
|
231
|
+
BigInt("0x" + String(part.rB).replace(/^0x/, "")),
|
|
232
|
+
context);
|
|
233
|
+
return { i: part.i, cA: part.cA, cB: part.cB, sig: sigOut(sig) };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ШАГ 3. СБОРКА. Складывать нечего: порядок частей и есть порядок битов, и он проверяется ниже.
|
|
237
|
+
function assembleProof(prepared, parts) {
|
|
238
|
+
const byIndex = new Map(parts.map((p) => [p.i, p]));
|
|
239
|
+
const proofs = [];
|
|
240
|
+
for (let i = 0; i < BITS; i++) {
|
|
241
|
+
const item = byIndex.get(i);
|
|
242
|
+
if (!item) throw new Error("assembleProof: нет части для бита " + i);
|
|
243
|
+
proofs.push({ cA: item.cA, cB: item.cB, sig: item.sig });
|
|
244
|
+
}
|
|
245
|
+
return { XA: prepared.XA, XB: prepared.XB, proofs };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// prove ОСТАЁТСЯ ТОЙ ЖЕ ФУНКЦИЕЙ и собрана из тех же трёх шагов: второй реализации одного и того же
|
|
249
|
+
// здесь не появилось - иначе проверки гоняли бы один код, а работал другой.
|
|
250
|
+
function prove(x, context, onBit) {
|
|
251
|
+
const prepared = prepareProof(x, context);
|
|
252
|
+
const parts = [];
|
|
253
|
+
for (let i = 0; i < BITS; i++) {
|
|
254
|
+
// Прогресс по ходу счёта. 253 бита - это те самые десять секунд, и без промежуточных сообщений
|
|
255
|
+
// интерфейс не может показать, что работа идёт, а не встала. Сообщаем каждые 16 бит и на последнем.
|
|
256
|
+
if (onBit && (i % 16 === 0 || i === BITS - 1)) onBit(i + 1, BITS);
|
|
257
|
+
parts.push(proveBit(prepared.bits[i], prepared.context));
|
|
258
|
+
}
|
|
259
|
+
return assembleProof(prepared, parts);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// СТРОГАЯ ПРОВЕРКА ПОДГРУППЫ, А НЕ ТОЛЬКО МАЛОГО ПОРЯДКА. Прежняя редакция проверяла [8]*P != 0:
|
|
263
|
+
// точку ЧИСТО малого порядка она отвергала, а точку с малой СОСТАВЛЯЮЩЕЙ (P = Q + T, где T порядка 2, 4
|
|
264
|
+
// или 8) пропускала - и вырожденное равенство проходило. Строгое условие: [l]*P = 0, где l - порядок
|
|
265
|
+
// подгруппы. secp256k1 не проверяется: у неё кофактор 1, и вся группа простого порядка.
|
|
266
|
+
function torsionFree(curve, point) {
|
|
267
|
+
if (curve.name !== "ed25519") return true;
|
|
268
|
+
// ЧТО ИМЕННО ЗДЕСЬ СЧИТАЕТСЯ. isTorsionFree() у @noble/curves - это ровно [n]*P = 0 (проверено по
|
|
269
|
+
// исходнику пакета: wnaf.mulUnsafe(this, CURVE.n).is0()), то есть точка лежит в подгруппе простого
|
|
270
|
+
// порядка. Своя попытка записать это через multiplyUnsafe(order) ПАДАЕТ: библиотека требует скаляр
|
|
271
|
+
// меньше порядка и на самом порядке бросает - то есть слагаемое проверки выглядело бы как отказ.
|
|
272
|
+
try { return point.isTorsionFree() === true; } catch { return false; }
|
|
273
|
+
}
|
|
274
|
+
// ФОРМА ПРОВЕРЯЕТСЯ ДО СЧЁТА. Прежний цикл шёл по длине присланного списка: короткое доказательство
|
|
275
|
+
// доказывало ДРУГОЕ представление, длинное - жгло время проверки (доказательство ~145 КБ и так считается
|
|
276
|
+
// секундами). Честный путь всегда шлёт ровно BITS частей. Неразбираемое поле возвращает отказ, а не
|
|
277
|
+
// исключение наружу: вызывающий обязан получать {ok:false}, а не падать.
|
|
278
|
+
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 };
|
|
283
|
+
const P = (curve, h) => curve.P.fromHex(String(h).replace(/^0x/, ""));
|
|
284
|
+
let XA, XB, csA, csB;
|
|
285
|
+
try {
|
|
286
|
+
XA = P(curves.A, proof.XA); XB = P(curves.B, proof.XB);
|
|
287
|
+
if (!torsionFree(curves.B, XB)) return { ok: false, reason: "доказательство: XB не лежит в подгруппе простого порядка" };
|
|
288
|
+
} catch (e) {
|
|
289
|
+
return { ok: false, reason: "доказательство: XA или XB не разбирается (" + String((e && e.message) || e) + ")" };
|
|
290
|
+
}
|
|
291
|
+
// НОМЕР БИТА НАЗЫВАЕТСЯ И ЗДЕСЬ: без него отказ «где-то в доказательстве» неотличим от порчи любого из
|
|
292
|
+
// 253 обязательств, а разбирать 145 КБ руками никто не станет.
|
|
293
|
+
try {
|
|
294
|
+
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)); }
|
|
296
|
+
});
|
|
297
|
+
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)); }
|
|
299
|
+
});
|
|
300
|
+
} catch (e) {
|
|
301
|
+
return { ok: false, reason: "доказательство: " + String((e && e.message) || e) };
|
|
302
|
+
}
|
|
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" };
|
|
305
|
+
for (let i = 0; i < proof.proofs.length; i++) {
|
|
306
|
+
const item = proof.proofs[i];
|
|
307
|
+
try {
|
|
308
|
+
if (!torsionFree(curves.B, csB[i])) return { ok: false, reason: "бит " + i + ": обязательство не лежит в подгруппе простого порядка" };
|
|
309
|
+
const cA = item.cA, cB = item.cB;
|
|
310
|
+
const sig = Object.fromEntries(Object.entries(item.sig).map(([k, v]) => [k, BigInt(String(v).startsWith("0x") ? v : "0x" + v)]));
|
|
311
|
+
const pA = P(curves.A, cA), pB = P(curves.B, cB);
|
|
312
|
+
const pre = (q1, q2) => [ctxUtf8(context), encP(pA), encP(pB), encP(q1), encP(q2)];
|
|
313
|
+
const aG = mul(curves.A, sig.a1, curves.A.Galt), eCA = mPublic(curves.A, pA, sig.eA);
|
|
314
|
+
const bH = mul(curves.B, sig.b1, curves.B.Galt), eCB = mPublic(curves.B, pB, sig.eB);
|
|
315
|
+
const eA = challenge(curves.A, pre(subP(aG, eCA), subP(bH, eCB)), context);
|
|
316
|
+
const eB = challenge(curves.B, pre(subP(aG, eCA), subP(bH, eCB)), context);
|
|
317
|
+
const aG0 = mul(curves.A, sig.a0, curves.A.Galt), ecA = mPublic(curves.A, subP(pA, curves.A.G), eA);
|
|
318
|
+
const bH0 = mul(curves.B, sig.b0, curves.B.Galt), ecB = mPublic(curves.B, subP(pB, curves.B.G), eB);
|
|
319
|
+
const eA0 = challenge(curves.A, pre(subP(aG0, ecA), subP(bH0, ecB)), context);
|
|
320
|
+
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 + ": вызовы не сошлись" };
|
|
322
|
+
} catch (e) {
|
|
323
|
+
return { ok: false, reason: "бит " + i + ": поля не разбираются (" + String((e && e.message) || e) + ")" };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return { ok: true };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return { BITS, curves, prove, verify, prepareProof, proveBit, assembleProof, torsionFree };
|
|
330
|
+
}
|