@ninsei-labs/sdk 0.39.3 → 0.40.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 +55 -0
- 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 +561 -451
- 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 +8 -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 +20 -8
- package/src/config.mjs +1 -1
- package/src/engine.mjs +4 -0
- 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 +1 -1
- package/src/swaps.mjs +1 -1
package/engine/atomic/halfenc.js
CHANGED
|
@@ -1,39 +1,26 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/halfenc.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
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// Transferring a key half: an encrypted half fit for publishing on chain.
|
|
6
|
+
// WHAT THIS IS. A half x is sent so that: third parties cannot read it (the mask comes from an ECDH shared secret
|
|
7
|
+
// with the recipient); THE RECIPIENT can check that the ciphertext holds the half matching the published point X by
|
|
8
|
+
// the simple equality x*G == X, with no separate ZK proof; and the data fits on chain.
|
|
9
|
+
// THIS IS OUR COMPOSITION, not from the paper: instead of an ECDSA adaptor signature we use ECDH + mask + equality
|
|
10
|
+
// check. The primitives are @noble/curves; the composition is ours.
|
|
11
|
+
// THE ADAPTOR-SIGNATURE DECISION IS MADE: NOT USED. Revealing the half goes by keccak commit/reveal (commitments
|
|
12
|
+
// before money, the contract verifies the half hash), and here the half is sent encrypted.
|
|
13
|
+
// IMPORTANT: the primitives are audited, THIS implementation is not.
|
|
14
|
+
// LOCKED UNTIL EXPLICIT PERMISSION: the encrypted-half envelope is not used in the live flow, and a dead key path
|
|
15
|
+
// nobody tested in a swap is locked. SEALING AND OPENING (seal/open) ARE LOCKED, NOT THE CONSTRUCTOR: the
|
|
16
|
+
// constructor is called by the live flow, so locking it would break the working path.
|
|
8
17
|
//
|
|
9
|
-
// ЧТО ЭТО. Половина x передаётся получателю так, что:
|
|
10
|
-
// 1) третьи лица прочитать её не могут (маска выводится из общего секрета ECDH с получателем);
|
|
11
|
-
// 2) ПОЛУЧАТЕЛЬ может проверить, что в шифртексте лежит именно та половина, которой соответствует
|
|
12
|
-
// опубликованная точка X - простым равенством (x*G == X), без отдельного ZK-доказательства;
|
|
13
|
-
// 3) данные помещаются в цепь: точка-получателя + эфемерная точка + замаскированный скаляр.
|
|
14
18
|
//
|
|
15
|
-
// ЭТО НАША КОМПОЗИЦИЯ, а не перенос из статьи. В документах протокола половина передаётся адаптерной
|
|
16
|
-
// подписью ECDSA (Binks, раздел про ecdsa adaptor verify/decrypt). Мы делаем проще и проверяемо:
|
|
17
|
-
// ECDH + маска + проверка равенством. Стойкость примитивов - на @noble/curves, но сам состав решения - наш.
|
|
18
19
|
//
|
|
19
|
-
// РЕШЕНИЕ ПО АДАПТЕРНОЙ ПОДПИСИ ПРИНЯТО: ОНА НЕ ПРИМЕНЯЕТСЯ, и это выбор, а не недоделка. Раскрытие половины
|
|
20
|
-
// идёт механизмом keccak commit/reveal (обязательства объявляются ДО денег, контракт сверяет хеш половины),
|
|
21
|
-
// а здесь половина передаётся зашифрованной. Разбор, выгоды, цена и причина - .hermes/docs/28-adaptor-note.md,
|
|
22
|
-
// раздел 8.
|
|
23
20
|
//
|
|
24
|
-
// ВАЖНО: примитивы аудированы, ЭТА реализация - нет.
|
|
25
21
|
|
|
26
|
-
// ЗАПЕРТО ДО ЯВНОГО РАЗРЕШЕНИЯ (вывод аудитора, review_01 §P3: «удалить или запереть мёртвую ветку, чтобы
|
|
27
|
-
// её не вызвали „на всякий случай“»). Посылка зашифрованной половины в живом потоке не участвует: раскрытие
|
|
28
|
-
// идёт механизмом keccak commit/reveal, а половину вторая сторона получает из цепи. Мёртвая ветка, которую
|
|
29
|
-
// можно позвать случайно, - это путь ключей, который никто не проверял в сделке, поэтому он заперт.
|
|
30
22
|
//
|
|
31
|
-
//
|
|
32
|
-
// придумана: конструктор зовёт ЖИВОЙ поток (www/js/atomic/order.js:26 создаёт механизм, :43 берёт у него
|
|
33
|
-
// ключ получателя), поэтому замок на конструкторе сломал бы рабочий путь. Мёртвое здесь - сам конверт:
|
|
34
|
-
// запечатать половину и вскрыть её.
|
|
35
|
-
// РАЗРЕШЕНИЕ ДАЁТСЯ ЯВНО: globalThis.ARRAKIS_ALLOW_HALFENC = true (так делает только проверка
|
|
36
|
-
// tools/check-halfenc.mjs, у которой на запертость есть отдельный случай).
|
|
23
|
+
// PERMISSION IS GIVEN EXPLICITLY: globalThis.ARRAKIS_ALLOW_HALFENC = true (only a check sets it, with a separate locked case).
|
|
37
24
|
export const HALFENC_LOCK_REASON =
|
|
38
25
|
"halfenc is locked: the sealed-half envelope is not part of the live flow (keccak commit/reveal replaced it). " +
|
|
39
26
|
"Set globalThis.ARRAKIS_ALLOW_HALFENC = true to work with it in a test or a spike.";
|
|
@@ -45,7 +32,7 @@ export function halfencUnlocked() {
|
|
|
45
32
|
export function createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes }) {
|
|
46
33
|
const ED_ORDER = ed25519.Point.Fn.ORDER;
|
|
47
34
|
const SEC_ORDER = secp256k1.Point.Fn.ORDER;
|
|
48
|
-
const LIMIT = ED_ORDER < SEC_ORDER ? ED_ORDER : SEC_ORDER; //
|
|
35
|
+
const LIMIT = ED_ORDER < SEC_ORDER ? ED_ORDER : SEC_ORDER; // mask within the smaller order
|
|
49
36
|
|
|
50
37
|
function bytesToBig(b) { let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v; }
|
|
51
38
|
function randScalar(limit) {
|
|
@@ -53,18 +40,18 @@ export function createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes }) {
|
|
|
53
40
|
const v = bytesToBig(randomBytes(32));
|
|
54
41
|
if (v > 0n && v < limit) return v;
|
|
55
42
|
}
|
|
56
|
-
throw new Error("randScalar:
|
|
43
|
+
throw new Error("randScalar: failed in 256 attempts");
|
|
57
44
|
}
|
|
58
45
|
const bigToHex = (v) => v.toString(16).padStart(64, "0");
|
|
59
46
|
|
|
60
|
-
//
|
|
47
|
+
// The encryption recipient key: published in advance and unrelated to the spend half.
|
|
61
48
|
function recipientKeyPair() {
|
|
62
49
|
const priv = randScalar(SEC_ORDER);
|
|
63
50
|
return { priv, pub: secp256k1.Point.BASE.multiply(priv).toHex() };
|
|
64
51
|
}
|
|
65
52
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
53
|
+
// The mask comes from the ECDH shared secret: the sender via its ephemeral pair and the recipient key, the
|
|
54
|
+
// recipient via its private key and the ephemeral point. Same formula.
|
|
68
55
|
function maskFrom(sharedPointHex, context) {
|
|
69
56
|
const h = keccak256(Uint8Array.from(Buffer.concat([
|
|
70
57
|
Buffer.from(sharedPointHex.replace(/^0x/, ""), "hex"),
|
|
@@ -73,48 +60,47 @@ export function createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes }) {
|
|
|
73
60
|
return bytesToBig(h) % LIMIT;
|
|
74
61
|
}
|
|
75
62
|
|
|
76
|
-
//
|
|
63
|
+
// SEAL: half x -> a publishable envelope for the recipient.
|
|
77
64
|
function seal(x, recipientPubHex, context) {
|
|
78
|
-
//
|
|
65
|
+
// LOCK ON THE ENVELOPE ITSELF: only the dead branch reaches it, so it cannot be called "just in case".
|
|
79
66
|
if (!halfencUnlocked()) throw new Error(HALFENC_LOCK_REASON);
|
|
80
|
-
if (!(x > 0n && x < LIMIT)) throw new Error("seal:
|
|
67
|
+
if (!(x > 0n && x < LIMIT)) throw new Error("seal: half outside min(l, n)");
|
|
81
68
|
const recipient = secp256k1.Point.fromHex(recipientPubHex.replace(/^0x/, ""));
|
|
82
69
|
const ephPriv = randScalar(SEC_ORDER);
|
|
83
70
|
const ephPub = secp256k1.Point.BASE.multiply(ephPriv);
|
|
84
|
-
const shared = recipient.multiply(ephPriv); // ECDH
|
|
71
|
+
const shared = recipient.multiply(ephPriv); // ECDH on the sender
|
|
85
72
|
const mask = maskFrom(shared.toHex(), context);
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// проверка: seal отдавал точки без префикса, а распаковка добавляла его).
|
|
73
|
+
// All hex fields are normalised: 0x prefix, lowercase. Else the envelope and the unpacked result would differ
|
|
74
|
+
// in representation of the same bytes.
|
|
89
75
|
const canon = (h) => "0x" + String(h).replace(/^0x/i, "").toLowerCase();
|
|
90
76
|
return {
|
|
91
|
-
recipient: canon(recipientPubHex), //
|
|
92
|
-
ephemeral: canon(ephPub.toHex()), //
|
|
93
|
-
masked: "0x" + bigToHex((x + mask) % LIMIT), //
|
|
77
|
+
recipient: canon(recipientPubHex), // to whom it is addressed
|
|
78
|
+
ephemeral: canon(ephPub.toHex()), // ephemeral point, needed by the recipient
|
|
79
|
+
masked: "0x" + bigToHex((x + mask) % LIMIT), // masked scalar
|
|
94
80
|
context: context || "",
|
|
95
81
|
};
|
|
96
82
|
}
|
|
97
83
|
|
|
98
|
-
//
|
|
84
|
+
// OPEN: the recipient restores the half and CHECKS it against the published point.
|
|
99
85
|
function open(sealed, recipientPriv, publicPointHex) {
|
|
100
|
-
//
|
|
86
|
+
// LOCK ON OPEN TOO: the second half of the same dead branch.
|
|
101
87
|
if (!halfencUnlocked()) return { ok: false, reason: HALFENC_LOCK_REASON };
|
|
102
|
-
if (!sealed || !sealed.ephemeral || !sealed.masked) return { ok: false, reason: "
|
|
88
|
+
if (!sealed || !sealed.ephemeral || !sealed.masked) return { ok: false, reason: "incomplete envelope" };
|
|
103
89
|
const eph = secp256k1.Point.fromHex(sealed.ephemeral.replace(/^0x/, ""));
|
|
104
|
-
const shared = eph.multiply(recipientPriv); // ECDH
|
|
90
|
+
const shared = eph.multiply(recipientPriv); // ECDH on the recipient
|
|
105
91
|
const mask = maskFrom(shared.toHex(), sealed.context);
|
|
106
92
|
const x = (BigInt(sealed.masked) - mask + LIMIT) % LIMIT;
|
|
107
|
-
if (x === 0n) return { ok: false, reason: "
|
|
93
|
+
if (x === 0n) return { ok: false, reason: "restored zero" };
|
|
108
94
|
if (!publicPointHex) return { ok: true, x };
|
|
109
|
-
//
|
|
95
|
+
// Check the binding "the ciphertext holds exactly this half": x*G must equal the published point.
|
|
110
96
|
const expect = ed25519.Point.BASE.multiply(x).toHex();
|
|
111
97
|
const got = ed25519.Point.fromHex((publicPointHex.startsWith("0x") ? publicPointHex.slice(2) : publicPointHex)).toHex();
|
|
112
|
-
if (expect !== got) return { ok: false, reason: "
|
|
98
|
+
if (expect !== got) return { ok: false, reason: "the ciphertext holds a different half" };
|
|
113
99
|
return { ok: true, x };
|
|
114
100
|
}
|
|
115
101
|
|
|
116
|
-
//
|
|
117
|
-
//
|
|
102
|
+
// The commitment for the contract: keccak256 of the envelope bytes. It lives inside the factory, with the hash
|
|
103
|
+
// passed in from outside.
|
|
118
104
|
function sealedCommitmentOf(hexBytes) {
|
|
119
105
|
const h = keccak256(hexToBytesArr(hexBytes));
|
|
120
106
|
return "0x" + bytesToHexStr(h);
|
|
@@ -123,21 +109,21 @@ export function createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes }) {
|
|
|
123
109
|
return { LIMIT, recipientKeyPair, seal, open, packSealed, unpackSealed, sealedCommitmentOf };
|
|
124
110
|
}
|
|
125
111
|
|
|
126
|
-
// ---
|
|
112
|
+
// --- Envelope byte format ---------------------------------------------------------------
|
|
127
113
|
//
|
|
128
|
-
//
|
|
129
|
-
// calldata
|
|
130
|
-
//
|
|
114
|
+
// The contract needs BYTES: their keccak256 goes into the order's sealedCommitment and the same bytes go into
|
|
115
|
+
// calldata at lock(). The format is fixed, because the commitment depends on it: any other layout changes the
|
|
116
|
+
// hash and the lock is rejected.
|
|
131
117
|
//
|
|
132
|
-
// 0 :
|
|
133
|
-
// 1..33 :
|
|
134
|
-
// 34..66 :
|
|
135
|
-
// 67..98 :
|
|
136
|
-
// 99 :
|
|
137
|
-
// 100.. :
|
|
118
|
+
// 0 : format version (1)
|
|
119
|
+
// 1..33 : encryption recipient public key (33 bytes, compressed secp256k1)
|
|
120
|
+
// 34..66 : sender ephemeral point (33 bytes, compressed)
|
|
121
|
+
// 67..98 : masked scalar (32 bytes, big-endian)
|
|
122
|
+
// 99 : context length in bytes (0..255)
|
|
123
|
+
// 100.. : order context in UTF-8
|
|
138
124
|
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
125
|
+
// Total 100 + context length bytes. JSON is deliberately not used: key order and number formatting would give
|
|
126
|
+
// different bytes for the same content, and the commitment requires uniqueness.
|
|
141
127
|
const FORMAT_VERSION = 1;
|
|
142
128
|
|
|
143
129
|
function bytesToHexStr(b) {
|
|
@@ -173,28 +159,28 @@ const hexToBytesArr = (h) => {
|
|
|
173
159
|
return out;
|
|
174
160
|
};
|
|
175
161
|
|
|
176
|
-
//
|
|
162
|
+
// Envelope -> bytes (hex). These bytes are hashed into the commitment and go into calldata.
|
|
177
163
|
export function packSealed(sealed) {
|
|
178
|
-
if (!sealed || !sealed.recipient || !sealed.ephemeral || !sealed.masked) throw new Error("packSealed:
|
|
164
|
+
if (!sealed || !sealed.recipient || !sealed.ephemeral || !sealed.masked) throw new Error("packSealed: incomplete envelope");
|
|
179
165
|
const recipient = hexToBytesArr(sealed.recipient);
|
|
180
166
|
const ephemeral = hexToBytesArr(sealed.ephemeral);
|
|
181
|
-
if (recipient.length !== 33) throw new Error("packSealed:
|
|
182
|
-
if (ephemeral.length !== 33) throw new Error("packSealed:
|
|
167
|
+
if (recipient.length !== 33) throw new Error("packSealed: recipient key must be 33 bytes (compressed point)");
|
|
168
|
+
if (ephemeral.length !== 33) throw new Error("packSealed: ephemeral point must be 33 bytes (compressed)");
|
|
183
169
|
const masked = BigInt(sealed.masked).toString(16).padStart(64, "0");
|
|
184
170
|
const ctx = utf8(sealed.context);
|
|
185
|
-
if (ctx.length > 255) throw new Error("packSealed:
|
|
171
|
+
if (ctx.length > 255) throw new Error("packSealed: context longer than 255 bytes");
|
|
186
172
|
return "0x" + bytesToHexStr(Uint8Array.of(FORMAT_VERSION))
|
|
187
173
|
+ bytesToHexStr(recipient) + bytesToHexStr(ephemeral) + masked
|
|
188
174
|
+ ctx.length.toString(16).padStart(2, "0") + bytesToHexStr(ctx);
|
|
189
175
|
}
|
|
190
176
|
|
|
191
|
-
//
|
|
177
|
+
// Bytes -> envelope. Checks the version and lengths; corrupted data is not parsed silently.
|
|
192
178
|
export function unpackSealed(hexBytes) {
|
|
193
179
|
const b = hexToBytesArr(hexBytes);
|
|
194
|
-
if (b.length < 100) throw new Error("unpackSealed:
|
|
195
|
-
if (b[0] !== FORMAT_VERSION) throw new Error("unpackSealed:
|
|
180
|
+
if (b.length < 100) throw new Error("unpackSealed: envelope shorter than 100 bytes");
|
|
181
|
+
if (b[0] !== FORMAT_VERSION) throw new Error("unpackSealed: unknown format version: " + b[0]);
|
|
196
182
|
const ctxLen = b[99];
|
|
197
|
-
if (b.length !== 100 + ctxLen) throw new Error("unpackSealed:
|
|
183
|
+
if (b.length !== 100 + ctxLen) throw new Error("unpackSealed: length does not match the one in the context");
|
|
198
184
|
return {
|
|
199
185
|
recipient: "0x" + bytesToHexStr(b.slice(1, 34)),
|
|
200
186
|
ephemeral: "0x" + bytesToHexStr(b.slice(34, 67)),
|
package/engine/atomic/halves.js
CHANGED
|
@@ -1,35 +1,24 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/halves.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
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// (статья, §4.3, формула 5: ks_i < min(l, n)). Это не деталь: именно из-за этого неравенства
|
|
13
|
-
// доказательство равенства логарифмов сходится в обеих группах.
|
|
14
|
-
//
|
|
15
|
-
// ВАЖНО ПРО БЕЗОПАСНОСТЬ. Аудированы примитивы (@noble/curves), а НЕ этот код. Наш слой поверх них
|
|
16
|
-
// проверки не проходил, и документация это фиксирует (§13). Не использовать для настоящих денег
|
|
17
|
-
// до отдельного разбора.
|
|
18
|
-
//
|
|
19
|
-
// Зависимости передаются снаружи, как в www/js/monero/address.js: в браузере - вендоренный бандл,
|
|
20
|
-
// в проверках - тот же пакет из node_modules. Модуль не тянет ничего сам.
|
|
5
|
+
// Half of a Monero key for the XMR <-> EVM atomic swap.
|
|
6
|
+
// The Monero spend key is the sum of two halves: ks = ks_a + ks_b (mod l); spending needs both.
|
|
7
|
+
// One half must work on edwards25519 (Monero) and secp256k1 (EVM), so the scalar stays below the smaller
|
|
8
|
+
// group order (min(l, n)) - that is what makes the equal-log proof converge on both groups.
|
|
9
|
+
// SECURITY: the audited primitives are @noble/curves, NOT this layer. Do not use with real money before a separate review.
|
|
10
|
+
// Dependencies are passed in from outside (browser: the vendored bundle; checks: node_modules). The module pulls nothing itself.
|
|
21
11
|
|
|
22
12
|
export function createHalves({ ed25519, secp256k1, keccak256, randomBytes }) {
|
|
23
13
|
for (const [name, v] of [["ed25519", ed25519], ["secp256k1", secp256k1], ["keccak256", keccak256], ["randomBytes", randomBytes]]) {
|
|
24
|
-
if (!v) throw new Error("createHalves:
|
|
14
|
+
if (!v) throw new Error("createHalves: not passed " + name);
|
|
25
15
|
}
|
|
26
16
|
const ED_ORDER = ed25519.Point.Fn.ORDER;
|
|
27
17
|
const SECP_ORDER = secp256k1.Point.Fn.ORDER;
|
|
28
|
-
//
|
|
18
|
+
// The smaller group order: a half must fit into both groups (paper section 4.3).
|
|
29
19
|
const LIMIT = ED_ORDER < SECP_ORDER ? ED_ORDER : SECP_ORDER;
|
|
30
20
|
|
|
31
|
-
//
|
|
32
|
-
// которое перекашивает вероятности в сторону малых значений.
|
|
21
|
+
// Rejection sampling gives a uniform distribution; a modulo reduction would skew it towards small values.
|
|
33
22
|
function newHalf() {
|
|
34
23
|
for (let i = 0; i < 256; i++) {
|
|
35
24
|
const bytes = randomBytes(32);
|
|
@@ -37,34 +26,28 @@ export function createHalves({ ed25519, secp256k1, keccak256, randomBytes }) {
|
|
|
37
26
|
for (const b of bytes) v = (v << 8n) | BigInt(b);
|
|
38
27
|
if (v > 0n && v < LIMIT) return v;
|
|
39
28
|
}
|
|
40
|
-
throw new Error("newHalf:
|
|
29
|
+
throw new Error("newHalf: could not get a scalar in 256 attempts");
|
|
41
30
|
}
|
|
42
31
|
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// и ту же половину, разные ордера - разные, посчитать может только владелец секрета.
|
|
32
|
+
// DERIVE THE HALF FROM A SECRET, NOT RANDOMNESS: the provider node derives it again from its key and the
|
|
33
|
+
// order binding, so the same order always gives the same half, and only the secret holder can compute it.
|
|
46
34
|
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// распределение обязано быть равномерным. Поэтому берём хеш, и если он не меньше порядка группы -
|
|
50
|
-
// меняем счётчик и хешируем снова. Счётчик входит в хеш, значит весь путь воспроизводим: тот же ордер
|
|
51
|
-
// при том же секрете даёт ту же половину и на той же попытке.
|
|
35
|
+
// Rejection sampling, not modulo, as in newHalf: the half feeds a Monero address and must be uniform.
|
|
36
|
+
// The attempt counter is part of the hash, so the same order and secret give the same half.
|
|
52
37
|
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
const VIEW_HALF_DOMAIN = "arrakis-order-view-v1"; // половина ПРОСМОТРА
|
|
38
|
+
// WHY keccak, NOT HMAC: keccak is a sponge, so the order domain/secret/binding/counter is safe, and the
|
|
39
|
+
// module needs no node:crypto (it runs in the browser too).
|
|
40
|
+
// Half domains. Quoting and payout both take them from here: if the strings diverge, the node derives a
|
|
41
|
+
// half that matches neither its commitment nor the assembled address.
|
|
42
|
+
const HALF_DOMAIN = "ninsei-order-half-v1"; // spend half
|
|
43
|
+
const VIEW_HALF_DOMAIN = "ninsei-order-view-v1"; // view half
|
|
60
44
|
|
|
61
45
|
function halfFromSeed(seedBytes, binding, domain = HALF_DOMAIN) {
|
|
62
|
-
if (!seedBytes || seedBytes.length < 16) throw new Error("halfFromSeed:
|
|
63
|
-
if (!binding) throw new Error("halfFromSeed:
|
|
46
|
+
if (!seedBytes || seedBytes.length < 16) throw new Error("halfFromSeed: secret shorter than 16 bytes");
|
|
47
|
+
if (!binding) throw new Error("halfFromSeed: order binding not set");
|
|
64
48
|
const dom = new TextEncoder().encode(domain + "|" + String(binding) + "|");
|
|
65
49
|
for (let counter = 0; counter < 256; counter++) {
|
|
66
|
-
//
|
|
67
|
-
// вписан трюком (инкремент последнего байта строки домена), и такой код читается как загадка.
|
|
50
|
+
// The attempt counter is an explicit 4-byte field, not a byte tweak.
|
|
68
51
|
const input = new Uint8Array(dom.length + seedBytes.length + 4);
|
|
69
52
|
input.set(dom, 0);
|
|
70
53
|
input.set(seedBytes, dom.length);
|
|
@@ -77,30 +60,26 @@ export function createHalves({ ed25519, secp256k1, keccak256, randomBytes }) {
|
|
|
77
60
|
for (const b of bytes) v = (v << 8n) | BigInt(b);
|
|
78
61
|
if (v > 0n && v < LIMIT) return v;
|
|
79
62
|
}
|
|
80
|
-
throw new Error("halfFromSeed:
|
|
63
|
+
throw new Error("halfFromSeed: could not get a scalar in 256 attempts");
|
|
81
64
|
}
|
|
82
65
|
|
|
83
66
|
function pointFromHex(P, hex) { return P.fromHex(hex.startsWith("0x") ? hex.slice(2) : hex); }
|
|
84
67
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// вызывающий, который читает файл, обязан получить половину строкой, а арифметика кривой требует
|
|
88
|
-
// bigint. Раньше это место принимало только bigint, и восстановление из НАСТОЯЩЕГО файла падало с
|
|
89
|
-
// "expected bigint, got string" - а проверка восстановления этого не видела, потому что подавала
|
|
90
|
-
// свежие половины. Приводим здесь, в одном месте, чтобы покрыть всех вызывающих сразу.
|
|
68
|
+
// A HALF MAY COME AS A STRING, AND THAT IS NORMAL: the recovery file stores halves as hex (BigInt cannot
|
|
69
|
+
// go into JSON). Convert here to cover all callers at once.
|
|
91
70
|
function toScalar(v) {
|
|
92
71
|
if (typeof v === "bigint") return v;
|
|
93
72
|
if (typeof v === "string") {
|
|
94
73
|
const t = v.trim().replace(/^0x/, "");
|
|
95
|
-
if (!/^[0-9a-fA-F]{1,64}$/.test(t)) throw new Error("
|
|
74
|
+
if (!/^[0-9a-fA-F]{1,64}$/.test(t)) throw new Error("half does not look like a number: " + v.slice(0, 12));
|
|
96
75
|
return BigInt("0x" + t);
|
|
97
76
|
}
|
|
98
77
|
if (typeof v === "number") return BigInt(v);
|
|
99
|
-
throw new Error("
|
|
78
|
+
throw new Error("half of unknown type: " + typeof v);
|
|
100
79
|
}
|
|
101
80
|
function hex(P) { return P.toHex(); }
|
|
102
81
|
|
|
103
|
-
//
|
|
82
|
+
// Public halves on both curves: Ks_i = ks_i*G (edwards25519), Bs_i = ks_i*H (secp256k1).
|
|
104
83
|
function publicHalves(half) {
|
|
105
84
|
return {
|
|
106
85
|
ed: hex(ed25519.Point.BASE.multiply(toScalar(half))),
|
|
@@ -108,8 +87,7 @@ export function createHalves({ ed25519, secp256k1, keccak256, randomBytes }) {
|
|
|
108
87
|
};
|
|
109
88
|
}
|
|
110
89
|
|
|
111
|
-
//
|
|
112
|
-
// на чём держится сборка общего адреса.
|
|
90
|
+
// Sum of halves: privately (mod l) and publicly (point addition). The second equality is what the joint address rests on.
|
|
113
91
|
function combineHalves(a, b) { return (toScalar(a) + toScalar(b)) % ED_ORDER; }
|
|
114
92
|
function combinePublic(edHexA, edHexB) {
|
|
115
93
|
return hex(pointFromHex(ed25519.Point, edHexA).add(pointFromHex(ed25519.Point, edHexB)));
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/order-client.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
|
-
//
|
|
5
|
+
// Worker client: turns message passing into an ordinary call with a progress callback.
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// A separate module is needed so the page knows nothing about postMessage and request ids:
|
|
8
|
+
// it calls buildSide(...) and gets a promise plus the stages.
|
|
10
9
|
export function createOrderWorker() {
|
|
11
|
-
if (typeof Worker === "undefined") throw new Error("
|
|
10
|
+
if (typeof Worker === "undefined") throw new Error("workers are unavailable in this environment");
|
|
12
11
|
const worker = new Worker(new URL("./order-worker.js", import.meta.url), { type: "module" });
|
|
13
12
|
const pending = new Map();
|
|
14
13
|
let seq = 0;
|
|
@@ -20,10 +19,10 @@ export function createOrderWorker() {
|
|
|
20
19
|
if (msg.kind === "progress") { entry.onProgress?.(msg.stage, msg.detail); return; }
|
|
21
20
|
pending.delete(msg.id);
|
|
22
21
|
if (msg.kind === "done") entry.resolve(msg.result);
|
|
23
|
-
else entry.reject(new Error(msg.message || "
|
|
22
|
+
else entry.reject(new Error(msg.message || "worker returned an error"));
|
|
24
23
|
};
|
|
25
24
|
worker.onerror = (event) => {
|
|
26
|
-
const err = new Error("
|
|
25
|
+
const err = new Error("worker crashed: " + (event.message || "no message"));
|
|
27
26
|
for (const [, entry] of pending) entry.reject(err);
|
|
28
27
|
pending.clear();
|
|
29
28
|
};
|
|
@@ -40,17 +39,17 @@ export function createOrderWorker() {
|
|
|
40
39
|
buildSide: (orderContext, onProgress) => call("newSide", { orderContext }, onProgress),
|
|
41
40
|
sealHalf: (orderContext, ownHalf, encPub, onProgress) => call("seal", { orderContext, ownHalf, counterparty: { encPub } }, onProgress),
|
|
42
41
|
verifyCounterparty: (orderContext, counterparty, sealedBytes, onProgress) => call("verify", { orderContext, counterparty, sealedBytes }, onProgress),
|
|
43
|
-
//
|
|
44
|
-
//
|
|
42
|
+
// Commitment to a half. Computed in the worker, because that is where the cryptography lives:
|
|
43
|
+
// only the hash goes out, the half stays inside and never reaches the main thread.
|
|
45
44
|
halfCommitment: (ownHalf) => call("commit", { ownHalf }),
|
|
46
|
-
//
|
|
45
|
+
// Point from a half: the counterparty's half comes from the quote, and the address needs a point.
|
|
47
46
|
pointOf: (ownHalf) => call("pointOf", { ownHalf }),
|
|
48
|
-
//
|
|
47
|
+
// Joint Monero address from the halves - in the worker, because that is where the cryptography lives.
|
|
49
48
|
jointAddress: (joint) => call("jointAddress", { joint }),
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// (order-worker.js, newMakerMaterial).
|
|
53
|
-
//
|
|
49
|
+
// MAKER MATERIAL OF THE REVERSE REQUEST: both halves, their points and the commitment. There is no proof
|
|
50
|
+
// here and there cannot be: in the reverse direction the node assigns the order binding, and there is
|
|
51
|
+
// nothing to prove before the ticket (order-worker.js, newMakerMaterial). The public part goes to the node
|
|
52
|
+
// in the request, the private part stays with the maker - it is used to claim the payout and see XMR arrival.
|
|
54
53
|
makerMaterial: () => call("newMakerMaterial"),
|
|
55
54
|
stop: () => worker.terminate(),
|
|
56
55
|
};
|