@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
|
@@ -0,0 +1,205 @@
|
|
|
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, 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
|
+
// Передача половины ключа: зашифрованная половина, пригодная для публикации в цепи.
|
|
7
|
+
// Разбор и обоснование: .hermes/docs/20-key-shares.md, разделы 5, 12.3, 16, 18.
|
|
8
|
+
//
|
|
9
|
+
// ЧТО ЭТО. Половина x передаётся получателю так, что:
|
|
10
|
+
// 1) третьи лица прочитать её не могут (маска выводится из общего секрета ECDH с получателем);
|
|
11
|
+
// 2) ПОЛУЧАТЕЛЬ может проверить, что в шифртексте лежит именно та половина, которой соответствует
|
|
12
|
+
// опубликованная точка X - простым равенством (x*G == X), без отдельного ZK-доказательства;
|
|
13
|
+
// 3) данные помещаются в цепь: точка-получателя + эфемерная точка + замаскированный скаляр.
|
|
14
|
+
//
|
|
15
|
+
// ЭТО НАША КОМПОЗИЦИЯ, а не перенос из статьи. В документах протокола половина передаётся адаптерной
|
|
16
|
+
// подписью ECDSA (Binks, раздел про ecdsa adaptor verify/decrypt). Мы делаем проще и проверяемо:
|
|
17
|
+
// ECDH + маска + проверка равенством. Стойкость примитивов - на @noble/curves, но сам состав решения - наш.
|
|
18
|
+
//
|
|
19
|
+
// РЕШЕНИЕ ПО АДАПТЕРНОЙ ПОДПИСИ ПРИНЯТО: ОНА НЕ ПРИМЕНЯЕТСЯ, и это выбор, а не недоделка. Раскрытие половины
|
|
20
|
+
// идёт механизмом keccak commit/reveal (обязательства объявляются ДО денег, контракт сверяет хеш половины),
|
|
21
|
+
// а здесь половина передаётся зашифрованной. Разбор, выгоды, цена и причина - .hermes/docs/28-adaptor-note.md,
|
|
22
|
+
// раздел 8.
|
|
23
|
+
//
|
|
24
|
+
// ВАЖНО: примитивы аудированы, ЭТА реализация - нет.
|
|
25
|
+
|
|
26
|
+
// ЗАПЕРТО ДО ЯВНОГО РАЗРЕШЕНИЯ (вывод аудитора, review_01 §P3: «удалить или запереть мёртвую ветку, чтобы
|
|
27
|
+
// её не вызвали „на всякий случай“»). Посылка зашифрованной половины в живом потоке не участвует: раскрытие
|
|
28
|
+
// идёт механизмом keccak commit/reveal, а половину вторая сторона получает из цепи. Мёртвая ветка, которую
|
|
29
|
+
// можно позвать случайно, - это путь ключей, который никто не проверял в сделке, поэтому он заперт.
|
|
30
|
+
//
|
|
31
|
+
// ЗАПЕРТО ИМЕННО СОЗДАНИЕ И ВСКРЫТИЕ КОНВЕРТА (seal/open), а НЕ КОНСТРУКТОР. Разница измерена, а не
|
|
32
|
+
// придумана: конструктор зовёт ЖИВОЙ поток (www/js/atomic/order.js:26 создаёт механизм, :43 берёт у него
|
|
33
|
+
// ключ получателя), поэтому замок на конструкторе сломал бы рабочий путь. Мёртвое здесь - сам конверт:
|
|
34
|
+
// запечатать половину и вскрыть её.
|
|
35
|
+
// РАЗРЕШЕНИЕ ДАЁТСЯ ЯВНО: globalThis.ARRAKIS_ALLOW_HALFENC = true (так делает только проверка
|
|
36
|
+
// tools/check-halfenc.mjs, у которой на запертость есть отдельный случай).
|
|
37
|
+
export const HALFENC_LOCK_REASON =
|
|
38
|
+
"halfenc is locked: the sealed-half envelope is not part of the live flow (keccak commit/reveal replaced it). " +
|
|
39
|
+
"Set globalThis.ARRAKIS_ALLOW_HALFENC = true to work with it in a test or a spike.";
|
|
40
|
+
|
|
41
|
+
export function halfencUnlocked() {
|
|
42
|
+
return globalThis.ARRAKIS_ALLOW_HALFENC === true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes }) {
|
|
46
|
+
const ED_ORDER = ed25519.Point.Fn.ORDER;
|
|
47
|
+
const SEC_ORDER = secp256k1.Point.Fn.ORDER;
|
|
48
|
+
const LIMIT = ED_ORDER < SEC_ORDER ? ED_ORDER : SEC_ORDER; // маскируем в меньшем порядке
|
|
49
|
+
|
|
50
|
+
function bytesToBig(b) { let v = 0n; for (const x of b) v = (v << 8n) | BigInt(x); return v; }
|
|
51
|
+
function randScalar(limit) {
|
|
52
|
+
for (let i = 0; i < 256; i++) {
|
|
53
|
+
const v = bytesToBig(randomBytes(32));
|
|
54
|
+
if (v > 0n && v < limit) return v;
|
|
55
|
+
}
|
|
56
|
+
throw new Error("randScalar: не удалось за 256 попыток");
|
|
57
|
+
}
|
|
58
|
+
const bigToHex = (v) => v.toString(16).padStart(64, "0");
|
|
59
|
+
|
|
60
|
+
// Ключ шифрования получателя: публикуется заранее и не связан с половиной ключа траты.
|
|
61
|
+
function recipientKeyPair() {
|
|
62
|
+
const priv = randScalar(SEC_ORDER);
|
|
63
|
+
return { priv, pub: secp256k1.Point.BASE.multiply(priv).toHex() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Маска выводится из общего секрета ECDH: у отправителя через свою эфемерную пару и ключ получателя,
|
|
67
|
+
// у получателя - через свой приватный ключ и эфемерную точку. Формула одна и та же.
|
|
68
|
+
function maskFrom(sharedPointHex, context) {
|
|
69
|
+
const h = keccak256(Uint8Array.from(Buffer.concat([
|
|
70
|
+
Buffer.from(sharedPointHex.replace(/^0x/, ""), "hex"),
|
|
71
|
+
Buffer.from(context || "", "utf8"),
|
|
72
|
+
])));
|
|
73
|
+
return bytesToBig(h) % LIMIT;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ЗАКРЫТИЕ: половина x -> публикуемая посылка для получателя.
|
|
77
|
+
function seal(x, recipientPubHex, context) {
|
|
78
|
+
// ЗАМОК НА САМОМ КОНВЕРТЕ: сюда попадает только мёртвая ветка, и «на всякий случай» позвать её нельзя.
|
|
79
|
+
if (!halfencUnlocked()) throw new Error(HALFENC_LOCK_REASON);
|
|
80
|
+
if (!(x > 0n && x < LIMIT)) throw new Error("seal: половина вне границ min(l, n)");
|
|
81
|
+
const recipient = secp256k1.Point.fromHex(recipientPubHex.replace(/^0x/, ""));
|
|
82
|
+
const ephPriv = randScalar(SEC_ORDER);
|
|
83
|
+
const ephPub = secp256k1.Point.BASE.multiply(ephPriv);
|
|
84
|
+
const shared = recipient.multiply(ephPriv); // ECDH у отправителя
|
|
85
|
+
const mask = maskFrom(shared.toHex(), context);
|
|
86
|
+
// Все hex-поля приводятся к одному виду: с префиксом 0x и в нижнем регистре. Иначе объект-посылка
|
|
87
|
+
// и результат распаковки различались бы представлением одних и тех же байт (на этом и споткнулась
|
|
88
|
+
// проверка: seal отдавал точки без префикса, а распаковка добавляла его).
|
|
89
|
+
const canon = (h) => "0x" + String(h).replace(/^0x/i, "").toLowerCase();
|
|
90
|
+
return {
|
|
91
|
+
recipient: canon(recipientPubHex), // кому адресовано
|
|
92
|
+
ephemeral: canon(ephPub.toHex()), // эфемерная точка, нужна получателю
|
|
93
|
+
masked: "0x" + bigToHex((x + mask) % LIMIT), // замаскированный скаляр
|
|
94
|
+
context: context || "",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ВСКРЫТИЕ: получатель восстанавливает половину и ПРОВЕРЯЕТ её по опубликованной точке.
|
|
99
|
+
function open(sealed, recipientPriv, publicPointHex) {
|
|
100
|
+
// ЗАМОК И НА ВСКРЫТИИ: вторая половина той же мёртвой ветки.
|
|
101
|
+
if (!halfencUnlocked()) return { ok: false, reason: HALFENC_LOCK_REASON };
|
|
102
|
+
if (!sealed || !sealed.ephemeral || !sealed.masked) return { ok: false, reason: "посылка неполная" };
|
|
103
|
+
const eph = secp256k1.Point.fromHex(sealed.ephemeral.replace(/^0x/, ""));
|
|
104
|
+
const shared = eph.multiply(recipientPriv); // ECDH у получателя
|
|
105
|
+
const mask = maskFrom(shared.toHex(), sealed.context);
|
|
106
|
+
const x = (BigInt(sealed.masked) - mask + LIMIT) % LIMIT;
|
|
107
|
+
if (x === 0n) return { ok: false, reason: "восстановлен ноль" };
|
|
108
|
+
if (!publicPointHex) return { ok: true, x };
|
|
109
|
+
// Проверка связки "в шифртексте именно эта половина": x*G обязан совпасть с опубликованной точкой.
|
|
110
|
+
const expect = ed25519.Point.BASE.multiply(x).toHex();
|
|
111
|
+
const got = ed25519.Point.fromHex((publicPointHex.startsWith("0x") ? publicPointHex.slice(2) : publicPointHex)).toHex();
|
|
112
|
+
if (expect !== got) return { ok: false, reason: "в шифртексте не та половина" };
|
|
113
|
+
return { ok: true, x };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Обязательство для контракта: keccak256 тех самых байтов посылки. Живёт внутри фабрики, потому что
|
|
117
|
+
// хеш-функция передаётся ей снаружи (как и все зависимости), и снаружи видно только через этот объект.
|
|
118
|
+
function sealedCommitmentOf(hexBytes) {
|
|
119
|
+
const h = keccak256(hexToBytesArr(hexBytes));
|
|
120
|
+
return "0x" + bytesToHexStr(h);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { LIMIT, recipientKeyPair, seal, open, packSealed, unpackSealed, sealedCommitmentOf };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// --- Байтовый формат посылки ---------------------------------------------------------------
|
|
127
|
+
//
|
|
128
|
+
// Контракту нужны БАЙТЫ: их keccak256 идёт в sealedCommitment ордера, и те же байты уходят в
|
|
129
|
+
// calldata при lock(). Формат фиксированный и однозначный, потому что от него зависит обязательство:
|
|
130
|
+
// при любой другой раскладке хеш не совпадёт и блокировка будет отвергнута.
|
|
131
|
+
//
|
|
132
|
+
// 0 : версия формата (1)
|
|
133
|
+
// 1..33 : публичный ключ получателя шифрования (33 байта, сжатая точка secp256k1)
|
|
134
|
+
// 34..66 : эфемерная точка отправителя (33 байта, сжатая)
|
|
135
|
+
// 67..98 : замаскированный скаляр (32 байта, big-endian)
|
|
136
|
+
// 99 : длина контекста в байтах (0..255)
|
|
137
|
+
// 100.. : контекст ордера в UTF-8
|
|
138
|
+
//
|
|
139
|
+
// Итого 100 + длина контекста байт. JSON здесь намеренно не используется: порядок ключей и форматирование
|
|
140
|
+
// чисел дали бы разные байты для одного и того же содержимого - а обязательство требует однозначности.
|
|
141
|
+
const FORMAT_VERSION = 1;
|
|
142
|
+
|
|
143
|
+
function bytesToHexStr(b) {
|
|
144
|
+
let s = "";
|
|
145
|
+
for (const x of b) s += x.toString(16).padStart(2, "0");
|
|
146
|
+
return s;
|
|
147
|
+
}
|
|
148
|
+
function utf8(str) {
|
|
149
|
+
const s = String(str == null ? "" : str);
|
|
150
|
+
const out = [];
|
|
151
|
+
for (let i = 0; i < s.length; i++) {
|
|
152
|
+
const c = s.charCodeAt(i);
|
|
153
|
+
if (c < 0x80) out.push(c);
|
|
154
|
+
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
|
155
|
+
else out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
156
|
+
}
|
|
157
|
+
return new Uint8Array(out);
|
|
158
|
+
}
|
|
159
|
+
function fromUtf8(b) {
|
|
160
|
+
let s = "";
|
|
161
|
+
for (let i = 0; i < b.length; i++) {
|
|
162
|
+
const c = b[i];
|
|
163
|
+
if (c < 0x80) s += String.fromCharCode(c);
|
|
164
|
+
else if (c < 0xe0) s += String.fromCharCode(((c & 0x1f) << 6) | (b[++i] & 0x3f));
|
|
165
|
+
else s += String.fromCharCode(((c & 0x0f) << 12) | ((b[++i] & 0x3f) << 6) | (b[++i] & 0x3f));
|
|
166
|
+
}
|
|
167
|
+
return s;
|
|
168
|
+
}
|
|
169
|
+
const hexToBytesArr = (h) => {
|
|
170
|
+
const s = String(h || "").replace(/^0x/i, "");
|
|
171
|
+
const out = new Uint8Array(s.length / 2);
|
|
172
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16);
|
|
173
|
+
return out;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Посылка -> байты (hex). Именно эти байты хешируются в обязательство и уходят в calldata.
|
|
177
|
+
export function packSealed(sealed) {
|
|
178
|
+
if (!sealed || !sealed.recipient || !sealed.ephemeral || !sealed.masked) throw new Error("packSealed: посылка неполная");
|
|
179
|
+
const recipient = hexToBytesArr(sealed.recipient);
|
|
180
|
+
const ephemeral = hexToBytesArr(sealed.ephemeral);
|
|
181
|
+
if (recipient.length !== 33) throw new Error("packSealed: ключ получателя должен быть 33 байта (сжатая точка)");
|
|
182
|
+
if (ephemeral.length !== 33) throw new Error("packSealed: эфемерная точка должна быть 33 байта (сжатая)");
|
|
183
|
+
const masked = BigInt(sealed.masked).toString(16).padStart(64, "0");
|
|
184
|
+
const ctx = utf8(sealed.context);
|
|
185
|
+
if (ctx.length > 255) throw new Error("packSealed: контекст длиннее 255 байт");
|
|
186
|
+
return "0x" + bytesToHexStr(Uint8Array.of(FORMAT_VERSION))
|
|
187
|
+
+ bytesToHexStr(recipient) + bytesToHexStr(ephemeral) + masked
|
|
188
|
+
+ ctx.length.toString(16).padStart(2, "0") + bytesToHexStr(ctx);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Байты -> посылка. Проверяет версию и длины; повреждённые данные не разбираются молча.
|
|
192
|
+
export function unpackSealed(hexBytes) {
|
|
193
|
+
const b = hexToBytesArr(hexBytes);
|
|
194
|
+
if (b.length < 100) throw new Error("unpackSealed: посылка короче 100 байт");
|
|
195
|
+
if (b[0] !== FORMAT_VERSION) throw new Error("unpackSealed: неизвестная версия формата: " + b[0]);
|
|
196
|
+
const ctxLen = b[99];
|
|
197
|
+
if (b.length !== 100 + ctxLen) throw new Error("unpackSealed: длина не совпадает с указанной в контексте");
|
|
198
|
+
return {
|
|
199
|
+
recipient: "0x" + bytesToHexStr(b.slice(1, 34)),
|
|
200
|
+
ephemeral: "0x" + bytesToHexStr(b.slice(34, 67)),
|
|
201
|
+
masked: "0x" + bytesToHexStr(b.slice(67, 99)),
|
|
202
|
+
context: fromUtf8(b.slice(100)),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
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, 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
|
+
// Половина ключа для атомарного свопа XMR <-> EVM.
|
|
7
|
+
// См. .hermes/docs/18-xmr-swap-protocol.md, разделы 2, 3, 12, 13.
|
|
8
|
+
//
|
|
9
|
+
// ЧТО ЭТО. Ключ траты Monero собирается как сумма двух половин: ks = ks_a + ks_b (mod l). Потратить
|
|
10
|
+
// выход можно, только зная обе половины. Одна и та же половина должна работать на двух кривых:
|
|
11
|
+
// edwards25519 (Monero) и secp256k1 (EVM) - поэтому скаляр берётся МЕНЬШЕ меньшего из порядков групп
|
|
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. Модуль не тянет ничего сам.
|
|
21
|
+
|
|
22
|
+
export function createHalves({ ed25519, secp256k1, keccak256, randomBytes }) {
|
|
23
|
+
for (const [name, v] of [["ed25519", ed25519], ["secp256k1", secp256k1], ["keccak256", keccak256], ["randomBytes", randomBytes]]) {
|
|
24
|
+
if (!v) throw new Error("createHalves: не передано " + name);
|
|
25
|
+
}
|
|
26
|
+
const ED_ORDER = ed25519.Point.Fn.ORDER;
|
|
27
|
+
const SECP_ORDER = secp256k1.Point.Fn.ORDER;
|
|
28
|
+
// Меньший из порядков: половина обязана помещаться в обе группы (статья §4.3).
|
|
29
|
+
const LIMIT = ED_ORDER < SECP_ORDER ? ED_ORDER : SECP_ORDER;
|
|
30
|
+
|
|
31
|
+
// Отбор с отбраковкой: даёт равномерное распределение, в отличие от приведения по модулю,
|
|
32
|
+
// которое перекашивает вероятности в сторону малых значений.
|
|
33
|
+
function newHalf() {
|
|
34
|
+
for (let i = 0; i < 256; i++) {
|
|
35
|
+
const bytes = randomBytes(32);
|
|
36
|
+
let v = 0n;
|
|
37
|
+
for (const b of bytes) v = (v << 8n) | BigInt(b);
|
|
38
|
+
if (v > 0n && v < LIMIT) return v;
|
|
39
|
+
}
|
|
40
|
+
throw new Error("newHalf: не удалось получить скаляр за 256 попыток");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ВЫВОД ПОЛОВИНЫ ИЗ СЕКРЕТА, А НЕ ИЗ СЛУЧАЙНОСТИ. Нужен ноде провайдера: она не хранит половину под
|
|
44
|
+
// каждый ордер, а выводит её заново из своего ключа и привязки к ордеру. Один и тот же ордер даёт одну
|
|
45
|
+
// и ту же половину, разные ордера - разные, посчитать может только владелец секрета.
|
|
46
|
+
//
|
|
47
|
+
// ОТБОР С ОТБРАКОВКОЙ, А НЕ ПРИВЕДЕНИЕ ПО МОДУЛЮ - по той же причине, что и в newHalf выше: приведение
|
|
48
|
+
// перекашивает распределение в сторону малых значений, а половина идёт в адрес Monero, где
|
|
49
|
+
// распределение обязано быть равномерным. Поэтому берём хеш, и если он не меньше порядка группы -
|
|
50
|
+
// меняем счётчик и хешируем снова. Счётчик входит в хеш, значит весь путь воспроизводим: тот же ордер
|
|
51
|
+
// при том же секрете даёт ту же половину и на той же попытке.
|
|
52
|
+
//
|
|
53
|
+
// ПОЧЕМУ keccak, А НЕ HMAC. keccak - губка, к удлинению сообщения она невосприимчива, поэтому порядок
|
|
54
|
+
// «домен, секрет, привязка, счётчик» безопасен и не даёт из одного вывода получить другой. Так модуль
|
|
55
|
+
// остаётся без зависимости от node:crypto и работает и в браузере.
|
|
56
|
+
// Домены половин. Отсюда их берут И котирование, И выплата: разойдись строки - и нода выведет половину,
|
|
57
|
+
// которой не соответствует ни её обязательство, ни собранный адрес, и обнаружит это только отказом.
|
|
58
|
+
const HALF_DOMAIN = "arrakis-order-half-v1"; // половина ТРАТЫ
|
|
59
|
+
const VIEW_HALF_DOMAIN = "arrakis-order-view-v1"; // половина ПРОСМОТРА
|
|
60
|
+
|
|
61
|
+
function halfFromSeed(seedBytes, binding, domain = HALF_DOMAIN) {
|
|
62
|
+
if (!seedBytes || seedBytes.length < 16) throw new Error("halfFromSeed: секрет короче 16 байт");
|
|
63
|
+
if (!binding) throw new Error("halfFromSeed: не задана привязка к ордеру");
|
|
64
|
+
const dom = new TextEncoder().encode(domain + "|" + String(binding) + "|");
|
|
65
|
+
for (let counter = 0; counter < 256; counter++) {
|
|
66
|
+
// Счётчик попытки лежит ЯВНО и четырьмя байтами, а не подменой соседнего байта: раньше он был
|
|
67
|
+
// вписан трюком (инкремент последнего байта строки домена), и такой код читается как загадка.
|
|
68
|
+
const input = new Uint8Array(dom.length + seedBytes.length + 4);
|
|
69
|
+
input.set(dom, 0);
|
|
70
|
+
input.set(seedBytes, dom.length);
|
|
71
|
+
input[dom.length + seedBytes.length] = (counter >>> 24) & 0xff;
|
|
72
|
+
input[dom.length + seedBytes.length + 1] = (counter >>> 16) & 0xff;
|
|
73
|
+
input[dom.length + seedBytes.length + 2] = (counter >>> 8) & 0xff;
|
|
74
|
+
input[dom.length + seedBytes.length + 3] = counter & 0xff;
|
|
75
|
+
const bytes = keccak256(input);
|
|
76
|
+
let v = 0n;
|
|
77
|
+
for (const b of bytes) v = (v << 8n) | BigInt(b);
|
|
78
|
+
if (v > 0n && v < LIMIT) return v;
|
|
79
|
+
}
|
|
80
|
+
throw new Error("halfFromSeed: не удалось получить скаляр за 256 попыток");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function pointFromHex(P, hex) { return P.fromHex(hex.startsWith("0x") ? hex.slice(2) : hex); }
|
|
84
|
+
|
|
85
|
+
// ПОЛОВИНА МОЖЕТ ПРИЙТИ СТРОКОЙ, И ЭТО НОРМАЛЬНО. Файл восстановления хранит половины hex-строками
|
|
86
|
+
// (BigInt нельзя положить в JSON - на этом уже один раз молча терялась запись сделки). Значит любой
|
|
87
|
+
// вызывающий, который читает файл, обязан получить половину строкой, а арифметика кривой требует
|
|
88
|
+
// bigint. Раньше это место принимало только bigint, и восстановление из НАСТОЯЩЕГО файла падало с
|
|
89
|
+
// "expected bigint, got string" - а проверка восстановления этого не видела, потому что подавала
|
|
90
|
+
// свежие половины. Приводим здесь, в одном месте, чтобы покрыть всех вызывающих сразу.
|
|
91
|
+
function toScalar(v) {
|
|
92
|
+
if (typeof v === "bigint") return v;
|
|
93
|
+
if (typeof v === "string") {
|
|
94
|
+
const t = v.trim().replace(/^0x/, "");
|
|
95
|
+
if (!/^[0-9a-fA-F]{1,64}$/.test(t)) throw new Error("половина не похожа на число: " + v.slice(0, 12));
|
|
96
|
+
return BigInt("0x" + t);
|
|
97
|
+
}
|
|
98
|
+
if (typeof v === "number") return BigInt(v);
|
|
99
|
+
throw new Error("половина неизвестного вида: " + typeof v);
|
|
100
|
+
}
|
|
101
|
+
function hex(P) { return P.toHex(); }
|
|
102
|
+
|
|
103
|
+
// Публичные половины на обеих кривых: Ks_i = ks_i*G (edwards25519), Bs_i = ks_i*H (secp256k1).
|
|
104
|
+
function publicHalves(half) {
|
|
105
|
+
return {
|
|
106
|
+
ed: hex(ed25519.Point.BASE.multiply(toScalar(half))),
|
|
107
|
+
secp: hex(secp256k1.Point.BASE.multiply(toScalar(half))),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Сумма половин: и приватно (mod l), и публично (сложение точек). Второе равенство - то,
|
|
112
|
+
// на чём держится сборка общего адреса.
|
|
113
|
+
function combineHalves(a, b) { return (toScalar(a) + toScalar(b)) % ED_ORDER; }
|
|
114
|
+
function combinePublic(edHexA, edHexB) {
|
|
115
|
+
return hex(pointFromHex(ed25519.Point, edHexA).add(pointFromHex(ed25519.Point, edHexB)));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { LIMIT, ED_ORDER, SECP_ORDER, HALF_DOMAIN, VIEW_HALF_DOMAIN, newHalf, halfFromSeed, publicHalves, combineHalves, combinePublic };
|
|
119
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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, 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
|
+
// Клиент воркера: превращает обмен сообщениями в обычный вызов с колбэком прогресса.
|
|
7
|
+
//
|
|
8
|
+
// Отдельный модуль нужен, чтобы страница не знала ничего про postMessage и идентификаторы запросов:
|
|
9
|
+
// она вызывает buildSide(...) и получает обещание плюс этапы.
|
|
10
|
+
export function createOrderWorker() {
|
|
11
|
+
if (typeof Worker === "undefined") throw new Error("воркеры недоступны в этом окружении");
|
|
12
|
+
const worker = new Worker(new URL("./order-worker.js", import.meta.url), { type: "module" });
|
|
13
|
+
const pending = new Map();
|
|
14
|
+
let seq = 0;
|
|
15
|
+
|
|
16
|
+
worker.onmessage = (event) => {
|
|
17
|
+
const msg = event.data || {};
|
|
18
|
+
const entry = pending.get(msg.id);
|
|
19
|
+
if (!entry) return;
|
|
20
|
+
if (msg.kind === "progress") { entry.onProgress?.(msg.stage, msg.detail); return; }
|
|
21
|
+
pending.delete(msg.id);
|
|
22
|
+
if (msg.kind === "done") entry.resolve(msg.result);
|
|
23
|
+
else entry.reject(new Error(msg.message || "воркер вернул ошибку"));
|
|
24
|
+
};
|
|
25
|
+
worker.onerror = (event) => {
|
|
26
|
+
const err = new Error("воркер упал: " + (event.message || "без сообщения"));
|
|
27
|
+
for (const [, entry] of pending) entry.reject(err);
|
|
28
|
+
pending.clear();
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const call = (type, args = {}, onProgress) => {
|
|
32
|
+
const id = ++seq;
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
pending.set(id, { resolve, reject, onProgress });
|
|
35
|
+
worker.postMessage({ id, type, ...args });
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
buildSide: (orderContext, onProgress) => call("newSide", { orderContext }, onProgress),
|
|
41
|
+
sealHalf: (orderContext, ownHalf, encPub, onProgress) => call("seal", { orderContext, ownHalf, counterparty: { encPub } }, onProgress),
|
|
42
|
+
verifyCounterparty: (orderContext, counterparty, sealedBytes, onProgress) => call("verify", { orderContext, counterparty, sealedBytes }, onProgress),
|
|
43
|
+
// Обязательство на половину. Считается в воркере, потому что там же живёт криптография: наружу
|
|
44
|
+
// уходит только хеш, половина остаётся внутри и в главный поток не попадает.
|
|
45
|
+
halfCommitment: (ownHalf) => call("commit", { ownHalf }),
|
|
46
|
+
// Точка по половине: половина контрагента приходит из котировки, а для адреса нужна точка.
|
|
47
|
+
pointOf: (ownHalf) => call("pointOf", { ownHalf }),
|
|
48
|
+
// Общий адрес Monero из половин - в воркере, потому что там же криптография.
|
|
49
|
+
jointAddress: (joint) => call("jointAddress", { joint }),
|
|
50
|
+
// МАТЕРИАЛ МЕЙКЕРА ОБРАТНОЙ ЗАЯВКИ: обе половины, их точки и обязательство. Доказательства здесь нет и
|
|
51
|
+
// быть не может: в обратном направлении привязку ордера назначает нода, и до билета доказывать нечего
|
|
52
|
+
// (order-worker.js, newMakerMaterial). Публичная часть уходит ноде в заявке, приватная остаётся у
|
|
53
|
+
// мейкера - ею забирают выплату и видят приход XMR.
|
|
54
|
+
makerMaterial: () => call("newMakerMaterial"),
|
|
55
|
+
stop: () => worker.terminate(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/order.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
|
+
// Сборка стороны ордера атомарного свопа: половина ключа, точки, доказательство, зашифрованная
|
|
7
|
+
// посылка и обязательство для контракта.
|
|
8
|
+
// Разбор: .hermes/docs/20-key-shares.md, разделы 12, 19, 23, 25.
|
|
9
|
+
//
|
|
10
|
+
// ЧТО ЭТО ЗАКРЫВАЕТ. До сих пор модули существовали по отдельности: половины, DLEQ, передача половины,
|
|
11
|
+
// байтовый формат. Здесь они собираются в одну сторону ордера и, главное, здесь КОДОМ выполняются два
|
|
12
|
+
// обязательных условия из раздела 20.5:
|
|
13
|
+
// 1) перед блокировкой проверяется, что в посылке контрагента лежит настоящая половина (сверка с точкой);
|
|
14
|
+
// 2) при этом проверяется и его DLEQ - что обе публичные точки стоят за одним скаляром.
|
|
15
|
+
// Без этих двух проверок схема не даёт гарантии, и они не «на будущее», а здесь.
|
|
16
|
+
//
|
|
17
|
+
// ЧЕСТНОЕ ОГРАНИЧЕНИЕ ДЕМО. Настоящего мейкера (RFQ-узла) ещё нет: в market.js котировки фиктивные.
|
|
18
|
+
// Поэтому сторона контрагента собирается ЛОКАЛЬНО и помечается как заглушка (standIn: true). Это
|
|
19
|
+
// позволяет прогнать полный цикл - от половины до обязательства и до вскрытия - но ценность гарантии
|
|
20
|
+
// появляется только с настоящим контрагентом, у которого свой ключ.
|
|
21
|
+
|
|
22
|
+
import { createHalves } from "./halves.js";import { halfCommitmentHex } from "./orderHalfCommit.js";
|
|
23
|
+
import { createDleq2 } from "./dleq2.js";
|
|
24
|
+
import { createHalfEnc, packSealed } from "./halfenc.js";
|
|
25
|
+
import { addressFromKeys } from "../monero/address.js";
|
|
26
|
+
import { swapAddressFromHalves } from "../monero/swapKeys.js";
|
|
27
|
+
|
|
28
|
+
export function createOrderBuilder({ ed25519, secp256k1, keccak256, sha3_512, randomBytes }) {
|
|
29
|
+
const halves = createHalves({ ed25519, secp256k1, keccak256, randomBytes });
|
|
30
|
+
const dleq = createDleq2({ ed25519, secp256k1, keccak256, sha3_512, randomBytes });
|
|
31
|
+
const enc = createHalfEnc({ secp256k1, ed25519, keccak256, randomBytes });
|
|
32
|
+
|
|
33
|
+
// Сторона ордера: ДВЕ половины (траты и просмотра), их точки, доказательство и ключ шифрования.
|
|
34
|
+
//
|
|
35
|
+
// ЗАЧЕМ ОТДЕЛЬНАЯ ПОЛОВИНА ПРОСМОТРА. Ключ просмотра в Monero независим от ключа траты, и из половины
|
|
36
|
+
// траты его не вывести. Без суммы половин ПРОСМОТРА кошелёк не найдёт входящие и не сможет сканировать:
|
|
37
|
+
// половины траты для скана недостаточно. При этом делиться половиной просмотра БЕЗОПАСНО - она даёт
|
|
38
|
+
// видеть и не даёт тратить; трата требует суммы половин ТРАТЫ.
|
|
39
|
+
//
|
|
40
|
+
// ДОКАЗАТЕЛЬСТВА ДЛЯ НЕЁ НЕ НУЖНО, и это осознанно: половина просмотра приходит вместе с котировкой и
|
|
41
|
+
// сама определяет точку просмотра, поэтому подменять её нечем. Доказательство DLEQ остаётся там, где
|
|
42
|
+
// оно решает - на половине ТРАТЫ, за которую платят ETH.
|
|
43
|
+
function newSide(orderContext, standIn = false, onBit) {
|
|
44
|
+
const half = halves.newHalf();
|
|
45
|
+
const pub = halves.publicHalves(half); // ed25519 и secp256k1
|
|
46
|
+
const viewHalf = halves.newHalf();
|
|
47
|
+
const viewPub = halves.publicHalves(viewHalf).ed; // точка просмотра: из неё собирается адрес
|
|
48
|
+
const encPair = enc.recipientKeyPair(); // отдельно от ключа траты
|
|
49
|
+
const proof = dleq.prove(half, orderContext, onBit); // привязано к контексту ордера
|
|
50
|
+
return { half, pub, viewHalf, viewPub, enc: encPair, proof, standIn };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Проверка стороны контрагента - это и есть условие (1) и (2) из раздела 20.5.
|
|
54
|
+
// Проверка стороны контрагента: доказательство и - если посылка дана - вскрытие ЕГО посылки НАШИМ
|
|
55
|
+
// приватным ключом со сверкой по ЕГО публичной точке.
|
|
56
|
+
//
|
|
57
|
+
// РОЛИ КЛЮЧЕЙ ЗДЕСЬ ПРИНЦИПИАЛЬНЫ, и первая версия этой функции их путала: она вскрывала посылку
|
|
58
|
+
// приватным ключом самого контрагента, которого у нас нет и быть не может - посылка адресована ему.
|
|
59
|
+
// Такая проверка не могла пройти никогда, но выглядела как работающая, что хуже всего.
|
|
60
|
+
function verifySide(side, orderContext, sealedPayloadBytes, ownEncPriv) {
|
|
61
|
+
const proofOk = dleq.verify(side.proof, orderContext);
|
|
62
|
+
if (!proofOk.ok) return { ok: false, reason: "DLEQ контрагента не прошёл: " + proofOk.reason };
|
|
63
|
+
// ДОКАЗАННАЯ ТОЧКА ОБЯЗАНА БЫТЬ ТОЙ ЖЕ, ЧТО УЙДЁТ В ОРДЕР, и этой сверки не было.
|
|
64
|
+
//
|
|
65
|
+
// Чего не хватало без неё: доказательство подтверждает, что за парой точек стоит ОДИН скаляр, но НЕ
|
|
66
|
+
// подтверждает, что доказанная точка ed25519 - та самая, по которой собирается общий адрес и которую
|
|
67
|
+
// контракт потом заставит раскрыть. Контрагент мог предъявить корректное доказательство для одной
|
|
68
|
+
// точки, а в ордер подставить другую. На своей стороне всё выглядело бы исправным, а расхождение
|
|
69
|
+
// вылезло бы в самом конце - при заборе XMR, когда исправлять уже нечем.
|
|
70
|
+
//
|
|
71
|
+
// Сверка одна на обе стороны: у своей стороны pub.ed выведена из половины, у контрагента приходит из
|
|
72
|
+
// котировки; в обоих случаях это ТА точка, которую увидят в контракте.
|
|
73
|
+
if (side.pub && side.pub.ed && String(side.proof.XB).toLowerCase() !== String(side.pub.ed).toLowerCase()) {
|
|
74
|
+
return { ok: false, reason: "доказанная точка ed25519 не совпадает с заявленной: " + side.proof.XB + " против " + side.pub.ed };
|
|
75
|
+
}
|
|
76
|
+
if (sealedPayloadBytes && ownEncPriv) {
|
|
77
|
+
const opened = enc.open(enc.unpackSealed(sealedPayloadBytes), ownEncPriv, side.pub.ed);
|
|
78
|
+
if (!opened.ok) return { ok: false, reason: "посылка контрагента не подтверждает половину: " + opened.reason };
|
|
79
|
+
}
|
|
80
|
+
return { ok: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Посылка со своей половиной, зашифрованная на ключ контрагента: то, что уходит ему и в контракт.
|
|
84
|
+
function sealFor(counterparty, ownHalf, orderContext) {
|
|
85
|
+
const sealed = enc.seal(ownHalf, counterparty.enc.pub, orderContext);
|
|
86
|
+
const bytes = packSealed(sealed);
|
|
87
|
+
return { sealed, bytes, commitment: enc.sealedCommitmentOf(bytes) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Открыть посылку контрагента своей половиной и проверить, что это именно его половина.
|
|
91
|
+
function openFrom(sealedBytes, ownEncPriv, counterpartyEdPub) {
|
|
92
|
+
return enc.open(enc.unpackSealed(sealedBytes), ownEncPriv, counterpartyEdPub);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ОБЯЗАТЕЛЬСТВО НА ПОЛОВИНУ - то, что контракт проверяет в `claim` и `refund`, и то, что входит в
|
|
96
|
+
// `termsHash`. Ровно это считает Solidity: `keccak256` от secp256k1-ТОЧКИ половины (`half·G`), а не от
|
|
97
|
+
// числа. Значит формула обязана совпадать у приложения и у ноды - поэтому она живёт ОТДЕЛЬНЫМ модулем и
|
|
98
|
+
// здесь только вызывается, а курс (`secp256k1`) передаётся снаружи: две копии одной формулы расходятся
|
|
99
|
+
// ровно тогда, когда цепь отвергнет «точно правильную» половину (#123).
|
|
100
|
+
function halfCommitment(half) {
|
|
101
|
+
return halfCommitmentHex(half, secp256k1, keccak256);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Общий адрес Monero: сумма публичных половин. Проверяется свойством (a+b)G == aG + bG.
|
|
105
|
+
function jointAddressPoint(sideA, sideB) {
|
|
106
|
+
return halves.combinePublic(sideA.pub.ed, sideB.pub.ed);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ТОЧКА ПО ПОЛОВИНЕ. Нужна там, где от контрагента пришла ПОЛОВИНА (так она приходит в котировке под
|
|
110
|
+
// ордер), а для адреса нужна точка. Точка из половины выводится тем же модулем половин, что и везде.
|
|
111
|
+
function pointOf(half) {
|
|
112
|
+
return halves.publicHalves(typeof half === "bigint" ? half : BigInt(half)).ed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ОБЩИЙ АДРЕС MONERO - то, ради чего схема и построена. Ни одна сторона не знает чужой половины ТРАТЫ:
|
|
116
|
+
// адрес собирается из СВОЕЙ половины и ЧУЖОЙ ТОЧКИ, потому что (a+b)G == aG + bG. До раскрытия второй
|
|
117
|
+
// половины расчётом адрес существует, но потратить с него не может никто.
|
|
118
|
+
//
|
|
119
|
+
// КОДИРОВЩИК БЕРЁМ ТОТ ЖЕ, ЧТО У КОШЕЛЬКА (monero/address.js). Второй кодировщик разошёлся бы с первым
|
|
120
|
+
// там, где ошибку видно только отправкой XMR, - и заметил бы её не тот, кто ошибся.
|
|
121
|
+
//
|
|
122
|
+
// КЛЮЧ ПРОСМОТРА СОБИРАЕТСЯ ОТДЕЛЬНО И ИЗ СВОИХ ПОЛОВИН: в Monero ключи просмотра и траты независимы, и
|
|
123
|
+
// подменять один другим нельзя. Без верной половины просмотра пользователь не увидит приход.
|
|
124
|
+
function jointAddress({ ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewPoint, network }) {
|
|
125
|
+
return swapAddressFromHalves({
|
|
126
|
+
halves, addressFromKeys, deps: { keccak256 },
|
|
127
|
+
ownSpendHalf, ownViewHalf, otherSpendPoint, otherViewPoint, network,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { halves, dleq, enc, newSide, verifySide, sealFor, openFrom, jointAddressPoint, pointOf, jointAddress, halfCommitment };
|
|
132
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/orderContext.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.
|
|
7
|
+
//
|
|
8
|
+
// ЗАЧЕМ ОТДЕЛЬНЫЙ МОДУЛЬ. Раньше контекст был произвольной строкой, которую передавал вызывающий, а
|
|
9
|
+
// заглушка контрагента просто дописывала к ней ":контрагент". Внутри одной вкладки это работало, но
|
|
10
|
+
// означало ровно одно: доказательство привязано НИ К ЧЕМУ, что снаружи воспроизвести нельзя. Наш же
|
|
11
|
+
// критерий приёмки (док 27, раздел 11) требует обратного: взял поля ордера с цепи, собрал точку,
|
|
12
|
+
// проверил доказательство, сошлось. Значит контекст обязан собираться ИЗ ПОЛЕЙ ОРДЕРА, и собираться
|
|
13
|
+
// одинаково у всех: у приложения, у ноды провайдера и у любого, кто захочет проверить сделку.
|
|
14
|
+
//
|
|
15
|
+
// ЧТО ВХОДИТ И ЧЕГО НЕ ВХОДИТ. Только УСЛОВИЯ ордера - то, что известно до всякого доказательства:
|
|
16
|
+
// сеть, фабрика, адрес внёсшего, адрес забирающего, сумма, сроки и соль.
|
|
17
|
+
//
|
|
18
|
+
// ОБЯЗАТЕЛЬСТВ И ТОЧЕК ЗДЕСЬ НЕТ, И ЭТО ИСПРАВЛЕНИЕ, А НЕ УПУЩЕНИЕ. Первая версия их включала, и выходил
|
|
19
|
+
// КРУГ: доказательство привязывается к контексту, а контекст брал обязательства и точки - то есть то, что
|
|
20
|
+
// само появляется только ВМЕСТЕ с доказательством. Порядок получался невыполнимым.
|
|
21
|
+
//
|
|
22
|
+
// Структура от этого не слабеет, и вот почему: половина провайдера ВЫВОДИТСЯ из условий ордера, поэтому
|
|
23
|
+
// одни и те же условия дают у него ту же половину, то же обязательство и ту же точку. Меняются условия -
|
|
24
|
+
// меняется всё. А обязательства и точки обеих сторон отдельно закреплены В САМОМ ОРДЕРЕ и в его
|
|
25
|
+
// termsHash: контракт делает их неизменяемыми после фондирования. Два разных места - две разные задачи.
|
|
26
|
+
//
|
|
27
|
+
// Адреса эскроу здесь тоже нет: он выводится из этих полей через CREATE2, то есть следствие, а не вход.
|
|
28
|
+
//
|
|
29
|
+
// ВЕРСИЯ КОНТЕКСТА. Префикс "arrakis-order-v1" отделяет контекст от любых других подписей и доказательств.
|
|
30
|
+
// Менять состав полей можно только вместе с номером версии в префиксе: иначе два разных смысла оказались
|
|
31
|
+
// бы под одной строкой, и проверка проходила бы там, где не должна.
|
|
32
|
+
|
|
33
|
+
// ВЕРСИЯ ПОДНЯТА ДО v2: состав полей изменился (убраны обязательства и точки), а по нашему же правилу
|
|
34
|
+
// смена смысла - это новая версия, а не дополнение. Выданных котировок в обращении ещё нет, поэтому
|
|
35
|
+
// переход ничего не ломает; дальше так уже не получится.
|
|
36
|
+
// ВЕРСИЯ 3: ИЗМЕНЁН ХЕШ ВЫЗОВА В ДОКАЗАТЕЛЬСТВЕ DLEQ (SHA3-512 и широкое приведение вместо keccak256).
|
|
37
|
+
// Это не косметика: доказательства версии 2 не проходят проверку версии 3, поэтому метка поднята - иначе
|
|
38
|
+
// живая котировка со старым доказательством ломалась бы молча, а причина искалась бы в подписи.
|
|
39
|
+
export const ORDER_CONTEXT_VERSION = "arrakis-order-v3";
|
|
40
|
+
|
|
41
|
+
const norm = (v) => String(v === undefined || v === null ? "" : v).trim().toLowerCase();
|
|
42
|
+
const dec = (v) => {
|
|
43
|
+
// Числа приводим к десятичной строке: "0x10" и "16" - одно и то же число, и если оставить как пришло,
|
|
44
|
+
// два вызывающих дадут разные контексты для одного ордера, а доказательство развалится без причины.
|
|
45
|
+
if (typeof v === "bigint") return v.toString(10);
|
|
46
|
+
if (typeof v === "number") return String(Math.trunc(v));
|
|
47
|
+
const s = norm(v);
|
|
48
|
+
if (s.startsWith("0x")) return BigInt(s).toString(10);
|
|
49
|
+
return s;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Поля перечислены ЯВНО и в фиксированном порядке. Порядок - часть протокола: переставить значит
|
|
53
|
+
// сменить контекст, то есть разом сделать недействительными все выданные котировки под ордер.
|
|
54
|
+
const FIELDS = [
|
|
55
|
+
"chainId", // сеть: один и тот же ордер в разных сетях - разные ордера
|
|
56
|
+
"factory", // какая фабрика создаёт эскроу
|
|
57
|
+
"locker", // кто вносит ETH
|
|
58
|
+
"claimer", // кому контракт заплатит
|
|
59
|
+
"amount", // сумма в wei
|
|
60
|
+
"readyBy", // срок готовности
|
|
61
|
+
"t1", // срок расчёта
|
|
62
|
+
"salt", // соль ордера
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export function orderContextString(order) {
|
|
66
|
+
if (!order || typeof order !== "object") throw new Error("для контекста ордера нужен объект с полями ордера");
|
|
67
|
+
const missing = FIELDS.filter((f) => order[f] === undefined || order[f] === null || order[f] === "");
|
|
68
|
+
if (missing.length) throw new Error("в контекст ордера не хватает полей: " + missing.join(", "));
|
|
69
|
+
return ORDER_CONTEXT_VERSION + "|" + FIELDS.map((f) => dec(order[f])).join("|");
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// GENERATED FILE - a byte-for-byte copy of the engine module www/js/atomic/orderHalfCommit.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
|
+
// ОБЯЗАТЕЛЬСТВО НА ПОЛОВИНУ - одна формула на все стороны.
|
|
7
|
+
//
|
|
8
|
+
// ЧТО СЧИТАЕТ ЦЕПЬ. `keccak256(x || y)` от secp256k1-ТОЧКИ половины `half·G` - то есть АДРЕС этой точки.
|
|
9
|
+
// При раскрытии контракт проверяет не «хеш числа совпал», а что предъявленная половина РАЗМНОЖАЕТСЯ в ту же
|
|
10
|
+
// точку (приём `ecrecover`, `NinseiEscrow.halfMatchesCommit`). Обязательство-ТОЧКА закрывает #123: раньше
|
|
11
|
+
// `keccak256(be32(half))` принимал любое число с совпавшим хешем, и вносящий ETH мог закоммитить хеш мусора,
|
|
12
|
+
// вернуть этим мусором ETH и запереть XMR второй стороны. Связь точки secp256k1 с точкой ed25519 (и адресом
|
|
13
|
+
// Monero) держит DLEQ - её проверяет рискующая сторона ДО платежа.
|
|
14
|
+
//
|
|
15
|
+
// Это же обязательство входит в termsHash и в котировку. Значит формула обязана совпадать у приложения, у
|
|
16
|
+
// ноды провайдера и у любого, кто станет проверять сделку. Две реализации одной формулы расходятся ровно
|
|
17
|
+
// тогда, когда это дороже всего: цепь отвергнет половину, которая «точно правильная».
|
|
18
|
+
//
|
|
19
|
+
// ТОЧКА БЕРЁТСЯ НЕСЖАТОЙ - 64 байта `x || y`, старшим байтом вперёд, как координаты ключа Ethereum.
|
|
20
|
+
// `secp256k1` и `keccak256` передаются снаружи: модуль не тянет зависимостей сам, как и соседние.
|
|
21
|
+
|
|
22
|
+
export function halfCommitmentHex(half, secp256k1, keccak256) {
|
|
23
|
+
if (!secp256k1 || !keccak256) throw new Error("нужны secp256k1 и keccak256");
|
|
24
|
+
const value = BigInt(half);
|
|
25
|
+
if (value <= 0n) throw new Error("половина должна быть положительным числом");
|
|
26
|
+
const raw = secp256k1.Point.BASE.multiply(value).toBytes(false); // 65 байт: 0x04 || x(32) || y(32)
|
|
27
|
+
const xy = raw.slice(1); // 64 байта: x || y
|
|
28
|
+
return "0x" + Array.from(keccak256(xy), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
29
|
+
}
|