@unlink-xyz/multisig 0.1.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.
Files changed (70) hide show
  1. package/dist/browser/index.js +29418 -0
  2. package/dist/browser/index.js.map +1 -0
  3. package/dist/src/frost/account.d.ts +39 -0
  4. package/dist/src/frost/account.d.ts.map +1 -0
  5. package/dist/src/frost/account.js +156 -0
  6. package/dist/src/frost/coordinator.d.ts +60 -0
  7. package/dist/src/frost/coordinator.d.ts.map +1 -0
  8. package/dist/src/frost/coordinator.js +211 -0
  9. package/dist/src/frost/crypto.d.ts +48 -0
  10. package/dist/src/frost/crypto.d.ts.map +1 -0
  11. package/dist/src/frost/crypto.js +79 -0
  12. package/dist/src/frost/curve.d.ts +97 -0
  13. package/dist/src/frost/curve.d.ts.map +1 -0
  14. package/dist/src/frost/curve.js +143 -0
  15. package/dist/src/frost/dkg.d.ts +36 -0
  16. package/dist/src/frost/dkg.d.ts.map +1 -0
  17. package/dist/src/frost/dkg.js +242 -0
  18. package/dist/src/frost/index.d.ts +16 -0
  19. package/dist/src/frost/index.d.ts.map +1 -0
  20. package/dist/src/frost/index.js +15 -0
  21. package/dist/src/frost/listener.d.ts +31 -0
  22. package/dist/src/frost/listener.d.ts.map +1 -0
  23. package/dist/src/frost/listener.js +65 -0
  24. package/dist/src/frost/mock-coordinator.d.ts +11 -0
  25. package/dist/src/frost/mock-coordinator.d.ts.map +1 -0
  26. package/dist/src/frost/mock-coordinator.js +288 -0
  27. package/dist/src/frost/serialization.d.ts +86 -0
  28. package/dist/src/frost/serialization.d.ts.map +1 -0
  29. package/dist/src/frost/serialization.js +193 -0
  30. package/dist/src/frost/signing.d.ts +64 -0
  31. package/dist/src/frost/signing.d.ts.map +1 -0
  32. package/dist/src/frost/signing.js +225 -0
  33. package/dist/src/frost/test-utils.d.ts +2 -0
  34. package/dist/src/frost/test-utils.d.ts.map +1 -0
  35. package/dist/src/frost/test-utils.js +1 -0
  36. package/dist/src/frost/types.d.ts +154 -0
  37. package/dist/src/frost/types.d.ts.map +1 -0
  38. package/dist/src/frost/types.js +4 -0
  39. package/dist/src/index.d.ts +9 -0
  40. package/dist/src/index.d.ts.map +1 -0
  41. package/dist/src/index.js +8 -0
  42. package/dist/src/node.d.ts +2 -0
  43. package/dist/src/node.d.ts.map +1 -0
  44. package/dist/src/node.js +1 -0
  45. package/dist/src/wallet/fs-store.d.ts +16 -0
  46. package/dist/src/wallet/fs-store.d.ts.map +1 -0
  47. package/dist/src/wallet/fs-store.js +62 -0
  48. package/dist/src/wallet/index.d.ts +5 -0
  49. package/dist/src/wallet/index.d.ts.map +1 -0
  50. package/dist/src/wallet/index.js +2 -0
  51. package/dist/src/wallet/indexeddb-store.d.ts +12 -0
  52. package/dist/src/wallet/indexeddb-store.d.ts.map +1 -0
  53. package/dist/src/wallet/indexeddb-store.js +69 -0
  54. package/dist/src/wallet/store.d.ts +14 -0
  55. package/dist/src/wallet/store.d.ts.map +1 -0
  56. package/dist/src/wallet/store.js +1 -0
  57. package/dist/src/wallet/types.d.ts +36 -0
  58. package/dist/src/wallet/types.d.ts.map +1 -0
  59. package/dist/src/wallet/types.js +1 -0
  60. package/dist/src/wallet/wallet.d.ts +3 -0
  61. package/dist/src/wallet/wallet.d.ts.map +1 -0
  62. package/dist/src/wallet/wallet.js +42 -0
  63. package/dist/tsconfig.tsbuildinfo +1 -0
  64. package/dist/tsup.browser.config.d.ts +7 -0
  65. package/dist/tsup.browser.config.d.ts.map +1 -0
  66. package/dist/tsup.browser.config.js +34 -0
  67. package/dist/vitest.config.d.ts +3 -0
  68. package/dist/vitest.config.d.ts.map +1 -0
  69. package/dist/vitest.config.js +20 -0
  70. package/package.json +59 -0
@@ -0,0 +1,288 @@
1
+ /**
2
+ * In-memory mock of the frost HTTP server for tests.
3
+ *
4
+ * Implements the same state machine as `backend/frost/src/state.rs`
5
+ * without requiring a Rust toolchain.
6
+ */
7
+ import { createServer, } from "node:http";
8
+ // --- Helpers ---
9
+ function readBody(req) {
10
+ return new Promise((resolve) => {
11
+ let data = "";
12
+ req.on("data", (chunk) => (data += chunk.toString()));
13
+ req.on("end", () => resolve(data));
14
+ });
15
+ }
16
+ function json(res, status, body) {
17
+ res.writeHead(status, { "Content-Type": "application/json" });
18
+ res.end(JSON.stringify(body));
19
+ }
20
+ function err(res, status, message) {
21
+ json(res, status, { error: message });
22
+ }
23
+ // --- Server ---
24
+ export async function startMockCoordinator() {
25
+ const dkgSessions = new Map();
26
+ const signingSessions = new Map();
27
+ let codeCounter = 0;
28
+ const nextCode = () => `mock-${++codeCounter}`;
29
+ const server = createServer(async (req, res) => {
30
+ const method = req.method ?? "GET";
31
+ // Strip /frost prefix so the mock works with both direct and gateway-style URLs
32
+ const rawUrl = req.url ?? "/";
33
+ const url = rawUrl.startsWith("/frost")
34
+ ? rawUrl.slice("/frost".length) || "/"
35
+ : rawUrl;
36
+ // --- DKG ---
37
+ if (method === "POST" && url === "/dkg/groups") {
38
+ const { threshold, total_participants } = JSON.parse(await readBody(req));
39
+ if (!threshold || threshold > total_participants)
40
+ return err(res, 400, "threshold must be in [1, total_participants]");
41
+ if (total_participants < 2)
42
+ return err(res, 400, "total_participants must be >= 2");
43
+ const code = nextCode();
44
+ dkgSessions.set(code, {
45
+ threshold,
46
+ totalParticipants: total_participants,
47
+ nextIndex: 2,
48
+ round1Packages: [],
49
+ round1Indices: new Set(),
50
+ round2Packages: [],
51
+ round2Indices: new Set(),
52
+ });
53
+ return json(res, 201, { code, status: "waiting_for_round1" });
54
+ }
55
+ const dkgJoin = url.match(/^\/dkg\/groups\/([^/]+)\/join$/);
56
+ if (method === "POST" && dkgJoin) {
57
+ const s = dkgSessions.get(dkgJoin[1]);
58
+ if (!s)
59
+ return err(res, 404, "not found");
60
+ if (s.nextIndex > s.totalParticipants)
61
+ return err(res, 400, "group is full");
62
+ const idx = s.nextIndex++;
63
+ return json(res, 200, {
64
+ participant_index: idx,
65
+ threshold: s.threshold,
66
+ total_participants: s.totalParticipants,
67
+ });
68
+ }
69
+ const dkgR1 = url.match(/^\/dkg\/groups\/([^/]+)\/round1$/);
70
+ if (dkgR1) {
71
+ const s = dkgSessions.get(dkgR1[1]);
72
+ if (!s)
73
+ return err(res, 404, "not found");
74
+ if (method === "GET") {
75
+ return json(res, 200, {
76
+ packages: s.round1Packages,
77
+ complete: s.round1Packages.length === s.totalParticipants,
78
+ });
79
+ }
80
+ if (method === "POST") {
81
+ const { participant_index, data } = JSON.parse(await readBody(req));
82
+ if (s.round1Packages.length === s.totalParticipants)
83
+ return err(res, 400, "not accepting round1 submissions");
84
+ if (s.round1Indices.has(participant_index))
85
+ return err(res, 409, "duplicate submission");
86
+ s.round1Packages.push(data);
87
+ s.round1Indices.add(participant_index);
88
+ const status = s.round1Packages.length === s.totalParticipants
89
+ ? "waiting_for_round2"
90
+ : "waiting_for_round1";
91
+ return json(res, 200, { status });
92
+ }
93
+ }
94
+ const dkgR2 = url.match(/^\/dkg\/groups\/([^/]+)\/round2$/);
95
+ if (dkgR2) {
96
+ const s = dkgSessions.get(dkgR2[1]);
97
+ if (!s)
98
+ return err(res, 404, "not found");
99
+ if (method === "GET") {
100
+ return json(res, 200, {
101
+ packages: s.round2Packages,
102
+ complete: s.round2Packages.length === s.totalParticipants,
103
+ });
104
+ }
105
+ if (method === "POST") {
106
+ const { participant_index, data } = JSON.parse(await readBody(req));
107
+ if (s.round1Packages.length < s.totalParticipants)
108
+ return err(res, 400, "not accepting round2 submissions");
109
+ if (s.round2Packages.length === s.totalParticipants)
110
+ return err(res, 400, "round2 already complete");
111
+ if (s.round2Indices.has(participant_index))
112
+ return err(res, 409, "duplicate submission");
113
+ s.round2Packages.push(data);
114
+ s.round2Indices.add(participant_index);
115
+ const status = s.round2Packages.length === s.totalParticipants
116
+ ? "complete"
117
+ : "waiting_for_round2";
118
+ return json(res, 200, { status });
119
+ }
120
+ }
121
+ const dkgStatus = url.match(/^\/dkg\/groups\/([^/]+)\/status$/);
122
+ if (method === "GET" && dkgStatus) {
123
+ const s = dkgSessions.get(dkgStatus[1]);
124
+ if (!s)
125
+ return err(res, 404, "not found");
126
+ let status = "waiting_for_round1";
127
+ if (s.round2Packages.length === s.totalParticipants)
128
+ status = "complete";
129
+ else if (s.round1Packages.length === s.totalParticipants)
130
+ status = "waiting_for_round2";
131
+ return json(res, 200, {
132
+ code: dkgStatus[1],
133
+ status,
134
+ threshold: s.threshold,
135
+ total_participants: s.totalParticipants,
136
+ round1_count: s.round1Packages.length,
137
+ round2_count: s.round2Packages.length,
138
+ });
139
+ }
140
+ // --- Signing ---
141
+ // GET /sign/sessions?group_id=X — list non-complete sessions
142
+ if (method === "GET" && url.startsWith("/sign/sessions")) {
143
+ const parsed = new URL(url, "http://localhost");
144
+ if (parsed.pathname === "/sign/sessions") {
145
+ const groupId = parsed.searchParams.get("group_id");
146
+ if (!groupId)
147
+ return err(res, 400, "group_id query parameter is required");
148
+ const sessions = [];
149
+ for (const [code, s] of signingSessions) {
150
+ const complete = s.shares.length === s.participants.length;
151
+ if (complete)
152
+ continue;
153
+ if (s.groupId !== groupId)
154
+ continue;
155
+ let status = "waiting_for_commitments";
156
+ if (s.commitments.length === s.participants.length)
157
+ status = "waiting_for_shares";
158
+ sessions.push({
159
+ code,
160
+ status,
161
+ participants: s.participants,
162
+ message: s.message,
163
+ group_id: s.groupId ?? null,
164
+ });
165
+ }
166
+ return json(res, 200, { sessions });
167
+ }
168
+ }
169
+ // GET /sign/sessions/:code — get session detail (before /commitments and /shares matching)
170
+ const sigDetail = url.match(/^\/sign\/sessions\/([^/]+)$/);
171
+ if (method === "GET" && sigDetail) {
172
+ const s = signingSessions.get(sigDetail[1]);
173
+ if (!s)
174
+ return err(res, 404, "not found");
175
+ let status = "waiting_for_commitments";
176
+ if (s.shares.length === s.participants.length)
177
+ status = "complete";
178
+ else if (s.commitments.length === s.participants.length)
179
+ status = "waiting_for_shares";
180
+ return json(res, 200, {
181
+ code: sigDetail[1],
182
+ status,
183
+ participants: s.participants,
184
+ message: s.message,
185
+ group_id: s.groupId ?? null,
186
+ });
187
+ }
188
+ if (method === "POST" && url === "/sign/sessions") {
189
+ const { participants, message, group_id } = JSON.parse(await readBody(req));
190
+ if (!Array.isArray(participants) || participants.length < 2)
191
+ return err(res, 400, "need at least 2 participants");
192
+ if (participants.includes(0))
193
+ return err(res, 400, "participant index must be >= 1");
194
+ const code = nextCode();
195
+ signingSessions.set(code, {
196
+ participants,
197
+ message,
198
+ groupId: group_id ?? undefined,
199
+ commitments: [],
200
+ commitmentIndices: new Set(),
201
+ shares: [],
202
+ shareIndices: new Set(),
203
+ });
204
+ return json(res, 201, { code, status: "waiting_for_commitments" });
205
+ }
206
+ const sigCommit = url.match(/^\/sign\/sessions\/([^/]+)\/commitments$/);
207
+ if (sigCommit) {
208
+ const s = signingSessions.get(sigCommit[1]);
209
+ if (!s)
210
+ return err(res, 404, "not found");
211
+ if (method === "GET") {
212
+ return json(res, 200, {
213
+ packages: s.commitments,
214
+ complete: s.commitments.length === s.participants.length,
215
+ });
216
+ }
217
+ if (method === "POST") {
218
+ const { participant_index, data } = JSON.parse(await readBody(req));
219
+ if (s.commitments.length === s.participants.length)
220
+ return err(res, 400, "not accepting commitments");
221
+ if (s.commitmentIndices.has(participant_index))
222
+ return err(res, 409, "duplicate submission");
223
+ s.commitments.push(data);
224
+ s.commitmentIndices.add(participant_index);
225
+ const status = s.commitments.length === s.participants.length
226
+ ? "waiting_for_shares"
227
+ : "waiting_for_commitments";
228
+ return json(res, 200, { status });
229
+ }
230
+ }
231
+ const sigShare = url.match(/^\/sign\/sessions\/([^/]+)\/shares$/);
232
+ if (sigShare) {
233
+ const s = signingSessions.get(sigShare[1]);
234
+ if (!s)
235
+ return err(res, 404, "not found");
236
+ if (method === "GET") {
237
+ return json(res, 200, {
238
+ packages: s.shares,
239
+ complete: s.shares.length === s.participants.length,
240
+ });
241
+ }
242
+ if (method === "POST") {
243
+ const { participant_index, data } = JSON.parse(await readBody(req));
244
+ if (s.commitments.length < s.participants.length)
245
+ return err(res, 400, "not accepting shares");
246
+ if (s.shares.length === s.participants.length)
247
+ return err(res, 400, "shares already complete");
248
+ if (s.shareIndices.has(participant_index))
249
+ return err(res, 409, "duplicate submission");
250
+ s.shares.push(data);
251
+ s.shareIndices.add(participant_index);
252
+ const status = s.shares.length === s.participants.length
253
+ ? "complete"
254
+ : "waiting_for_shares";
255
+ return json(res, 200, { status });
256
+ }
257
+ }
258
+ const sigStatus = url.match(/^\/sign\/sessions\/([^/]+)\/status$/);
259
+ if (method === "GET" && sigStatus) {
260
+ const s = signingSessions.get(sigStatus[1]);
261
+ if (!s)
262
+ return err(res, 404, "not found");
263
+ let status = "waiting_for_commitments";
264
+ if (s.shares.length === s.participants.length)
265
+ status = "complete";
266
+ else if (s.commitments.length === s.participants.length)
267
+ status = "waiting_for_shares";
268
+ return json(res, 200, {
269
+ code: sigStatus[1],
270
+ status,
271
+ expected_participants: s.participants.length,
272
+ commitment_count: s.commitments.length,
273
+ share_count: s.shares.length,
274
+ message: s.message,
275
+ });
276
+ }
277
+ err(res, 404, "not found");
278
+ });
279
+ return new Promise((resolve) => {
280
+ server.listen(0, "127.0.0.1", () => {
281
+ const addr = server.address();
282
+ resolve({
283
+ url: `http://127.0.0.1:${addr.port}`,
284
+ close: () => new Promise((r) => server.close(() => r())),
285
+ });
286
+ });
287
+ });
288
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Serialization for FROST types over HTTP/JSON.
3
+ *
4
+ * Convention (per RFC 9591, ciphersuite-specific):
5
+ * - bigint -> "0x..." hex string (EVM convention)
6
+ * - Point -> ["0x...", "0x..."]
7
+ * - Uint8Array -> base64 string
8
+ */
9
+ import { type Point } from "./curve.js";
10
+ import type { DkgRound1Package, DkgRound2Package, FrostConfig, MultisigAccount, SignatureShare, SigningCommitment } from "./types.js";
11
+ export declare function serializeBigint(n: bigint): string;
12
+ export declare function deserializeBigint(s: string): bigint;
13
+ export declare function serializePoint(p: Point): [string, string];
14
+ export declare function deserializePoint(arr: [string, string]): Point;
15
+ export declare function serializeBytes(bytes: Uint8Array): string;
16
+ export declare function deserializeBytes(b64: string): Uint8Array;
17
+ type SerializedSchnorrProof = {
18
+ commitment: [string, string];
19
+ response: string;
20
+ };
21
+ type SerializedEncryptedShare = {
22
+ fromIndex: number;
23
+ toIndex: number;
24
+ ciphertext: string;
25
+ nonce: string;
26
+ };
27
+ type SerializedDkgRound1Package = {
28
+ participantIndex: number;
29
+ coefficientCommitments: [string, string][];
30
+ proof: SerializedSchnorrProof;
31
+ };
32
+ type SerializedEncryptedViewingKey = {
33
+ toIndex: number;
34
+ ciphertext: string;
35
+ nonce: string;
36
+ };
37
+ type SerializedDkgRound2Package = {
38
+ participantIndex: number;
39
+ encryptedShares: SerializedEncryptedShare[];
40
+ encryptedViewingKeys?: SerializedEncryptedViewingKey[];
41
+ };
42
+ type SerializedSigningCommitment = {
43
+ participantIndex: number;
44
+ hiding: [string, string];
45
+ binding: [string, string];
46
+ };
47
+ type SerializedSignatureShare = {
48
+ participantIndex: number;
49
+ share: string;
50
+ };
51
+ export declare function serializeDkgRound1Package(pkg: DkgRound1Package): SerializedDkgRound1Package;
52
+ export declare function deserializeDkgRound1Package(data: SerializedDkgRound1Package): DkgRound1Package;
53
+ export declare function serializeDkgRound2Package(pkg: DkgRound2Package): SerializedDkgRound2Package;
54
+ export declare function deserializeDkgRound2Package(data: SerializedDkgRound2Package): DkgRound2Package;
55
+ export declare function serializeSigningCommitment(c: SigningCommitment): SerializedSigningCommitment;
56
+ export declare function deserializeSigningCommitment(data: SerializedSigningCommitment): SigningCommitment;
57
+ export declare function serializeSignatureShare(s: SignatureShare): SerializedSignatureShare;
58
+ export declare function deserializeSignatureShare(data: SerializedSignatureShare): SignatureShare;
59
+ export declare const MULTISIG_ACCOUNT_VERSION = 1;
60
+ type SerializedKeyShare = {
61
+ participantIndex: number;
62
+ secretShare: string;
63
+ verificationShare: [string, string];
64
+ };
65
+ type SerializedViewingKeyPair = {
66
+ privateKey: string;
67
+ pubkey: string;
68
+ };
69
+ export type SerializedMultisigAccount = {
70
+ version: typeof MULTISIG_ACCOUNT_VERSION;
71
+ groupId: string;
72
+ config: FrostConfig;
73
+ participantIndex: number;
74
+ keyShare: SerializedKeyShare;
75
+ groupPublicKey: [string, string];
76
+ coefficientCommitments: [number, [string, string][]][];
77
+ viewingKeyPair: SerializedViewingKeyPair;
78
+ nullifyingKey: string;
79
+ masterPublicKey: string;
80
+ gatewayUrl: string;
81
+ address: string;
82
+ };
83
+ export declare function serializeMultisigAccount(account: MultisigAccount): SerializedMultisigAccount;
84
+ export declare function deserializeMultisigAccount(data: SerializedMultisigAccount): MultisigAccount;
85
+ export {};
86
+ //# sourceMappingURL=serialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialization.d.ts","sourceRoot":"","sources":["../../../src/frost/serialization.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Serialization for FROST types over HTTP/JSON.
3
+ *
4
+ * Convention (per RFC 9591, ciphersuite-specific):
5
+ * - bigint -> "0x..." hex string (EVM convention)
6
+ * - Point -> ["0x...", "0x..."]
7
+ * - Uint8Array -> base64 string
8
+ */
9
+ import { babyjubjub } from "./curve.js";
10
+ // === Primitives ===
11
+ export function serializeBigint(n) {
12
+ return "0x" + n.toString(16);
13
+ }
14
+ export function deserializeBigint(s) {
15
+ if (!s.startsWith("0x"))
16
+ throw new Error(`expected hex string, got: ${s}`);
17
+ return BigInt(s);
18
+ }
19
+ export function serializePoint(p) {
20
+ return [serializeBigint(p[0]), serializeBigint(p[1])];
21
+ }
22
+ export function deserializePoint(arr) {
23
+ const p = [deserializeBigint(arr[0]), deserializeBigint(arr[1])];
24
+ if (!babyjubjub.isOnCurve(p)) {
25
+ throw new Error("deserialized point is not on the curve");
26
+ }
27
+ if (babyjubjub.isIdentity(p)) {
28
+ throw new Error("deserialized point is the identity");
29
+ }
30
+ return p;
31
+ }
32
+ export function serializeBytes(bytes) {
33
+ // Use btoa-compatible approach that works in Node and browser
34
+ let binary = "";
35
+ for (let i = 0; i < bytes.length; i++) {
36
+ binary += String.fromCharCode(bytes[i]);
37
+ }
38
+ return btoa(binary);
39
+ }
40
+ export function deserializeBytes(b64) {
41
+ const binary = atob(b64);
42
+ const bytes = new Uint8Array(binary.length);
43
+ for (let i = 0; i < binary.length; i++) {
44
+ bytes[i] = binary.charCodeAt(i);
45
+ }
46
+ return bytes;
47
+ }
48
+ // DkgRound1Package
49
+ export function serializeDkgRound1Package(pkg) {
50
+ return {
51
+ participantIndex: pkg.participantIndex,
52
+ coefficientCommitments: pkg.coefficientCommitments.map(serializePoint),
53
+ proof: {
54
+ commitment: serializePoint(pkg.proof.commitment),
55
+ response: serializeBigint(pkg.proof.response),
56
+ },
57
+ };
58
+ }
59
+ export function deserializeDkgRound1Package(data) {
60
+ return {
61
+ participantIndex: data.participantIndex,
62
+ coefficientCommitments: data.coefficientCommitments.map(deserializePoint),
63
+ proof: {
64
+ commitment: deserializePoint(data.proof.commitment),
65
+ response: deserializeBigint(data.proof.response),
66
+ },
67
+ };
68
+ }
69
+ // DkgRound2Package
70
+ export function serializeDkgRound2Package(pkg) {
71
+ const result = {
72
+ participantIndex: pkg.participantIndex,
73
+ encryptedShares: pkg.encryptedShares.map((s) => ({
74
+ fromIndex: s.fromIndex,
75
+ toIndex: s.toIndex,
76
+ ciphertext: serializeBytes(s.ciphertext),
77
+ nonce: serializeBytes(s.nonce),
78
+ })),
79
+ };
80
+ if (pkg.encryptedViewingKeys) {
81
+ result.encryptedViewingKeys = pkg.encryptedViewingKeys.map((k) => ({
82
+ toIndex: k.toIndex,
83
+ ciphertext: serializeBytes(k.ciphertext),
84
+ nonce: serializeBytes(k.nonce),
85
+ }));
86
+ }
87
+ return result;
88
+ }
89
+ export function deserializeDkgRound2Package(data) {
90
+ const result = {
91
+ participantIndex: data.participantIndex,
92
+ encryptedShares: data.encryptedShares.map((s) => ({
93
+ fromIndex: s.fromIndex,
94
+ toIndex: s.toIndex,
95
+ ciphertext: deserializeBytes(s.ciphertext),
96
+ nonce: deserializeBytes(s.nonce),
97
+ })),
98
+ };
99
+ if (data.encryptedViewingKeys) {
100
+ result.encryptedViewingKeys = data.encryptedViewingKeys.map((k) => ({
101
+ toIndex: k.toIndex,
102
+ ciphertext: deserializeBytes(k.ciphertext),
103
+ nonce: deserializeBytes(k.nonce),
104
+ }));
105
+ }
106
+ return result;
107
+ }
108
+ // SigningCommitment
109
+ export function serializeSigningCommitment(c) {
110
+ return {
111
+ participantIndex: c.participantIndex,
112
+ hiding: serializePoint(c.hiding),
113
+ binding: serializePoint(c.binding),
114
+ };
115
+ }
116
+ export function deserializeSigningCommitment(data) {
117
+ return {
118
+ participantIndex: data.participantIndex,
119
+ hiding: deserializePoint(data.hiding),
120
+ binding: deserializePoint(data.binding),
121
+ };
122
+ }
123
+ // SignatureShare
124
+ export function serializeSignatureShare(s) {
125
+ return {
126
+ participantIndex: s.participantIndex,
127
+ share: serializeBigint(s.share),
128
+ };
129
+ }
130
+ export function deserializeSignatureShare(data) {
131
+ return {
132
+ participantIndex: data.participantIndex,
133
+ share: deserializeBigint(data.share),
134
+ };
135
+ }
136
+ // MultisigAccount
137
+ export const MULTISIG_ACCOUNT_VERSION = 1;
138
+ export function serializeMultisigAccount(account) {
139
+ const coeffEntries = [];
140
+ for (const [idx, points] of account.coefficientCommitments) {
141
+ coeffEntries.push([idx, points.map(serializePoint)]);
142
+ }
143
+ return {
144
+ version: MULTISIG_ACCOUNT_VERSION,
145
+ groupId: account.groupId,
146
+ config: account.config,
147
+ participantIndex: account.participantIndex,
148
+ keyShare: {
149
+ participantIndex: account.keyShare.participantIndex,
150
+ secretShare: serializeBigint(account.keyShare.secretShare),
151
+ verificationShare: serializePoint(account.keyShare.verificationShare),
152
+ },
153
+ groupPublicKey: serializePoint(account.groupPublicKey),
154
+ coefficientCommitments: coeffEntries,
155
+ viewingKeyPair: {
156
+ privateKey: serializeBytes(account.viewingKeyPair.privateKey),
157
+ pubkey: serializeBytes(account.viewingKeyPair.pubkey),
158
+ },
159
+ nullifyingKey: serializeBigint(account.nullifyingKey),
160
+ masterPublicKey: serializeBigint(account.masterPublicKey),
161
+ gatewayUrl: account.gatewayUrl,
162
+ address: account.address,
163
+ };
164
+ }
165
+ export function deserializeMultisigAccount(data) {
166
+ if (data.version !== MULTISIG_ACCOUNT_VERSION) {
167
+ throw new Error(`unsupported multisig account version ${data.version}`);
168
+ }
169
+ const coefficientCommitments = new Map();
170
+ for (const [idx, serializedPoints] of data.coefficientCommitments) {
171
+ coefficientCommitments.set(idx, serializedPoints.map(deserializePoint));
172
+ }
173
+ return {
174
+ groupId: data.groupId,
175
+ config: data.config,
176
+ participantIndex: data.participantIndex,
177
+ keyShare: {
178
+ participantIndex: data.keyShare.participantIndex,
179
+ secretShare: deserializeBigint(data.keyShare.secretShare),
180
+ verificationShare: deserializePoint(data.keyShare.verificationShare),
181
+ },
182
+ groupPublicKey: deserializePoint(data.groupPublicKey),
183
+ coefficientCommitments,
184
+ viewingKeyPair: {
185
+ privateKey: deserializeBytes(data.viewingKeyPair.privateKey),
186
+ pubkey: deserializeBytes(data.viewingKeyPair.pubkey),
187
+ },
188
+ nullifyingKey: deserializeBigint(data.nullifyingKey),
189
+ masterPublicKey: deserializeBigint(data.masterPublicKey),
190
+ gatewayUrl: data.gatewayUrl,
191
+ address: data.address,
192
+ };
193
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Generalized m-of-n FROST signing protocol.
3
+ *
4
+ * Reimplements core signing from poc.ts with:
5
+ * - Any m-of-n threshold support
6
+ * - Mandatory per-share verification (for ROAST compatibility)
7
+ * - Nonce reuse protection
8
+ *
9
+ * References:
10
+ * - RFC 9591: https://datatracker.ietf.org/doc/html/rfc9591
11
+ * - FROST Paper: https://eprint.iacr.org/2020/852.pdf
12
+ */
13
+ import { type Point } from "./curve.js";
14
+ import type { FrostSignature, GroupPublicKey, KeyShare, SignatureShare, SigningCommitment, SigningNonces } from "./types.js";
15
+ /**
16
+ * Compute Lagrange coefficient for participant i at x=0.
17
+ *
18
+ * lambda_i = product_{j in participants, j != i} (0 - j) / (i - j)
19
+ */
20
+ export declare function lagrangeCoefficient(participantIndex: number, allParticipants: number[]): bigint;
21
+ /**
22
+ * Round 1: Generate nonces and commitments.
23
+ * Caller MUST store nonces securely and never reuse them.
24
+ */
25
+ export declare function generateCommitment(participantIndex: number): {
26
+ nonces: SigningNonces;
27
+ commitment: SigningCommitment;
28
+ };
29
+ /**
30
+ * Compute binding factor for a participant.
31
+ * rho_i = H(i || message || encoded_commitments)
32
+ */
33
+ export declare function computeBindingFactor(participantIndex: number, message: bigint, commitments: SigningCommitment[]): bigint;
34
+ /**
35
+ * Compute the group commitment R from all participants' commitments.
36
+ * R = sum(D_i + rho_i * E_i) for all participants
37
+ */
38
+ export declare function computeGroupCommitment(commitments: SigningCommitment[], bindingFactors: Map<number, bigint>): Point;
39
+ /**
40
+ * Round 2: Generate signature share.
41
+ * z_i = d_i + rho_i * e_i + 8 * c * lambda_i * s_i
42
+ *
43
+ * Nonces are zeroed after use to prevent reuse.
44
+ */
45
+ export declare function generateSignatureShare(keyShare: KeyShare, nonces: SigningNonces, message: bigint, commitments: SigningCommitment[], groupPublicKey: GroupPublicKey): SignatureShare;
46
+ /**
47
+ * Verify a single signature share before aggregation.
48
+ * z_i * G == D_i + rho_i * E_i + 8 * c * lambda_i * V_i
49
+ */
50
+ export declare function verifySignatureShare(share: SignatureShare, commitment: SigningCommitment, verificationShare: Point, message: bigint, allCommitments: SigningCommitment[], groupPublicKey: GroupPublicKey): boolean;
51
+ /**
52
+ * Aggregate signature shares into final FROST signature.
53
+ * Optionally verifies each share before aggregation.
54
+ */
55
+ export declare function aggregateSignatures(commitments: SigningCommitment[], shares: SignatureShare[], message: bigint, options?: {
56
+ verificationShares?: Map<number, Point>;
57
+ groupPublicKey?: GroupPublicKey;
58
+ }): FrostSignature;
59
+ /**
60
+ * Verify a FROST signature.
61
+ * S * G == R + 8 * c * Y
62
+ */
63
+ export declare function verifyFrostSignature(signature: FrostSignature, message: bigint, groupPublicKey: GroupPublicKey): boolean;
64
+ //# sourceMappingURL=signing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signing.d.ts","sourceRoot":"","sources":["../../../src/frost/signing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAOL,KAAK,KAAK,EACX,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EACd,QAAQ,EACR,cAAc,EACd,iBAAiB,EACjB,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,gBAAgB,EAAE,MAAM,EACxB,eAAe,EAAE,MAAM,EAAE,GACxB,MAAM,CAgBR;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,gBAAgB,EAAE,MAAM,GAAG;IAC5D,MAAM,EAAE,aAAa,CAAC;IACtB,UAAU,EAAE,iBAAiB,CAAC;CAC/B,CAkBA;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,gBAAgB,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,iBAAiB,EAAE,GAC/B,MAAM,CA0BR;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,iBAAiB,EAAE,EAChC,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAClC,KAAK,CAWP;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,iBAAiB,EAAE,EAChC,cAAc,EAAE,cAAc,GAC7B,cAAc,CAiDhB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,cAAc,EACrB,UAAU,EAAE,iBAAiB,EAC7B,iBAAiB,EAAE,KAAK,EACxB,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,iBAAiB,EAAE,EACnC,cAAc,EAAE,cAAc,GAC7B,OAAO,CA6CT;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,iBAAiB,EAAE,EAChC,MAAM,EAAE,cAAc,EAAE,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE;IACR,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IACxC,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC,GACA,cAAc,CA0DhB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,cAAc,EACzB,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,cAAc,GAC7B,OAAO,CAiBT"}