@bcts/frost-hubert 1.0.0-alpha.17

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 (114) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +35 -0
  3. package/dist/bin/frost.cjs +109 -0
  4. package/dist/bin/frost.cjs.map +1 -0
  5. package/dist/bin/frost.d.cts +1 -0
  6. package/dist/bin/frost.d.mts +1 -0
  7. package/dist/bin/frost.mjs +109 -0
  8. package/dist/bin/frost.mjs.map +1 -0
  9. package/dist/chunk-CQwRTUmo.cjs +53 -0
  10. package/dist/chunk-D3JzZLW2.mjs +21 -0
  11. package/dist/cmd/index.cjs +45 -0
  12. package/dist/cmd/index.d.cts +4 -0
  13. package/dist/cmd/index.d.mts +4 -0
  14. package/dist/cmd/index.mjs +7 -0
  15. package/dist/cmd-C8pmNd28.mjs +4664 -0
  16. package/dist/cmd-C8pmNd28.mjs.map +1 -0
  17. package/dist/cmd-CxUgryx_.cjs +4803 -0
  18. package/dist/cmd-CxUgryx_.cjs.map +1 -0
  19. package/dist/dkg/index.cjs +7 -0
  20. package/dist/dkg/index.d.cts +2 -0
  21. package/dist/dkg/index.d.mts +2 -0
  22. package/dist/dkg/index.mjs +3 -0
  23. package/dist/dkg-D4RcblWl.cjs +364 -0
  24. package/dist/dkg-D4RcblWl.cjs.map +1 -0
  25. package/dist/dkg-DqGrAV81.mjs +334 -0
  26. package/dist/dkg-DqGrAV81.mjs.map +1 -0
  27. package/dist/frost/index.cjs +37 -0
  28. package/dist/frost/index.d.cts +207 -0
  29. package/dist/frost/index.d.cts.map +1 -0
  30. package/dist/frost/index.d.mts +207 -0
  31. package/dist/frost/index.d.mts.map +1 -0
  32. package/dist/frost/index.mjs +3 -0
  33. package/dist/frost-CMH1K0Cw.cjs +511 -0
  34. package/dist/frost-CMH1K0Cw.cjs.map +1 -0
  35. package/dist/frost-Csp0IOrd.mjs +326 -0
  36. package/dist/frost-Csp0IOrd.mjs.map +1 -0
  37. package/dist/index-BGVoWW5P.d.cts +172 -0
  38. package/dist/index-BGVoWW5P.d.cts.map +1 -0
  39. package/dist/index-BJeUYrdE.d.mts +396 -0
  40. package/dist/index-BJeUYrdE.d.mts.map +1 -0
  41. package/dist/index-ByMDUYKw.d.mts +1098 -0
  42. package/dist/index-ByMDUYKw.d.mts.map +1 -0
  43. package/dist/index-DejLkr_F.d.mts +172 -0
  44. package/dist/index-DejLkr_F.d.mts.map +1 -0
  45. package/dist/index-Dib1OE-e.d.cts +1098 -0
  46. package/dist/index-Dib1OE-e.d.cts.map +1 -0
  47. package/dist/index-DnvBKgec.d.cts +396 -0
  48. package/dist/index-DnvBKgec.d.cts.map +1 -0
  49. package/dist/index.cjs +85 -0
  50. package/dist/index.cjs.map +1 -0
  51. package/dist/index.d.cts +15 -0
  52. package/dist/index.d.cts.map +1 -0
  53. package/dist/index.d.mts +15 -0
  54. package/dist/index.d.mts.map +1 -0
  55. package/dist/index.mjs +24 -0
  56. package/dist/index.mjs.map +1 -0
  57. package/dist/registry/index.cjs +13 -0
  58. package/dist/registry/index.d.cts +2 -0
  59. package/dist/registry/index.d.mts +2 -0
  60. package/dist/registry/index.mjs +3 -0
  61. package/dist/registry-CBjRRqNv.mjs +144 -0
  62. package/dist/registry-CBjRRqNv.mjs.map +1 -0
  63. package/dist/registry-CWp2amuo.mjs +789 -0
  64. package/dist/registry-CWp2amuo.mjs.map +1 -0
  65. package/dist/registry-D5yh293y.cjs +857 -0
  66. package/dist/registry-D5yh293y.cjs.map +1 -0
  67. package/dist/registry-DNUNW6SH.cjs +163 -0
  68. package/dist/registry-DNUNW6SH.cjs.map +1 -0
  69. package/package.json +119 -0
  70. package/src/bin/frost.ts +218 -0
  71. package/src/cmd/busy.ts +64 -0
  72. package/src/cmd/check.ts +20 -0
  73. package/src/cmd/common.ts +40 -0
  74. package/src/cmd/dkg/common.ts +275 -0
  75. package/src/cmd/dkg/coordinator/finalize.ts +592 -0
  76. package/src/cmd/dkg/coordinator/index.ts +12 -0
  77. package/src/cmd/dkg/coordinator/invite.ts +217 -0
  78. package/src/cmd/dkg/coordinator/round1.ts +889 -0
  79. package/src/cmd/dkg/coordinator/round2.ts +959 -0
  80. package/src/cmd/dkg/index.ts +11 -0
  81. package/src/cmd/dkg/participant/finalize.ts +575 -0
  82. package/src/cmd/dkg/participant/index.ts +12 -0
  83. package/src/cmd/dkg/participant/receive.ts +348 -0
  84. package/src/cmd/dkg/participant/round1.ts +464 -0
  85. package/src/cmd/dkg/participant/round2.ts +627 -0
  86. package/src/cmd/index.ts +18 -0
  87. package/src/cmd/parallel.ts +334 -0
  88. package/src/cmd/registry/index.ts +88 -0
  89. package/src/cmd/registry/owner/index.ts +9 -0
  90. package/src/cmd/registry/owner/set.ts +70 -0
  91. package/src/cmd/registry/participant/add.ts +70 -0
  92. package/src/cmd/registry/participant/index.ts +9 -0
  93. package/src/cmd/sign/common.ts +108 -0
  94. package/src/cmd/sign/coordinator/index.ts +11 -0
  95. package/src/cmd/sign/coordinator/invite.ts +431 -0
  96. package/src/cmd/sign/coordinator/round1.ts +751 -0
  97. package/src/cmd/sign/coordinator/round2.ts +836 -0
  98. package/src/cmd/sign/index.ts +11 -0
  99. package/src/cmd/sign/participant/finalize.ts +823 -0
  100. package/src/cmd/sign/participant/index.ts +12 -0
  101. package/src/cmd/sign/participant/receive.ts +378 -0
  102. package/src/cmd/sign/participant/round1.ts +479 -0
  103. package/src/cmd/sign/participant/round2.ts +748 -0
  104. package/src/cmd/storage.ts +116 -0
  105. package/src/dkg/group-invite.ts +414 -0
  106. package/src/dkg/index.ts +10 -0
  107. package/src/dkg/proposed-participant.ts +132 -0
  108. package/src/frost/index.ts +456 -0
  109. package/src/index.ts +45 -0
  110. package/src/registry/group-record.ts +392 -0
  111. package/src/registry/index.ts +12 -0
  112. package/src/registry/owner-record.ts +146 -0
  113. package/src/registry/participant-record.ts +186 -0
  114. package/src/registry/registry-impl.ts +364 -0
@@ -0,0 +1,836 @@
1
+ /**
2
+ * Sign coordinator round 2 command.
3
+ *
4
+ * Port of cmd/sign/coordinator/round2.rs from frost-hubert-rust.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
10
+
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+
14
+ import {
15
+ type ARID,
16
+ type XID,
17
+ XID as XIDClass,
18
+ ARID as ARIDClass,
19
+ Signature,
20
+ type PrivateKeys,
21
+ JSON as JSONComponent,
22
+ } from "@bcts/components";
23
+ import { Envelope } from "@bcts/envelope";
24
+ import { type XIDDocument } from "@bcts/xid";
25
+
26
+ import { Registry, resolveRegistryPath } from "../../../registry/index.js";
27
+ import { parallelFetch, parallelSend, type CollectionResult } from "../../parallel.js";
28
+ import { type StorageClient } from "../../storage.js";
29
+ import { parseAridUr, signingKeyFromVerifying } from "../../dkg/common.js";
30
+ import { signingStateDir, SignFinalizeContent } from "../common.js";
31
+ import { putWithIndicator } from "../../busy.js";
32
+ import {
33
+ aggregateSignatures,
34
+ createSigningPackage,
35
+ deserializeSigningCommitments,
36
+ deserializeSignatureShare,
37
+ deserializePublicKeyPackage,
38
+ identifierFromU16,
39
+ serializeSignature,
40
+ serializeSignatureShare,
41
+ type SerializedPublicKeyPackage,
42
+ type SerializedSigningCommitments,
43
+ type FrostIdentifier,
44
+ type Ed25519SigningCommitments,
45
+ type Ed25519SignatureShare,
46
+ type FrostPublicKeyPackage,
47
+ } from "../../../frost/index.js";
48
+
49
+ /**
50
+ * Options for the sign round2 command.
51
+ */
52
+ export interface SignRound2Options {
53
+ registryPath?: string;
54
+ groupId?: string;
55
+ sessionId: string;
56
+ parallel?: boolean;
57
+ timeoutSeconds?: number;
58
+ previewFinalize?: boolean;
59
+ verbose?: boolean;
60
+ }
61
+
62
+ /**
63
+ * Result of the sign round2 command.
64
+ */
65
+ export interface SignRound2Result {
66
+ signature: string;
67
+ signedEnvelope: string;
68
+ accepted: number;
69
+ rejected: number;
70
+ errors: number;
71
+ timeouts: number;
72
+ }
73
+
74
+ /**
75
+ * Data extracted from a successful signature share response.
76
+ *
77
+ * Port of `struct SignRound2ResponseData` from cmd/sign/coordinator/round2.rs.
78
+ */
79
+ interface SignRound2ResponseData {
80
+ signatureShare: Ed25519SignatureShare;
81
+ finalizeArid: ARID;
82
+ }
83
+
84
+ /**
85
+ * State loaded from start.json.
86
+ *
87
+ * Port of `struct StartState` from cmd/sign/coordinator/round2.rs.
88
+ */
89
+ interface StartState {
90
+ groupId: ARID;
91
+ minSigners: number;
92
+ participants: XID[];
93
+ targetUr: string;
94
+ }
95
+
96
+ /**
97
+ * Individual participant's commitment data.
98
+ *
99
+ * Port of `struct ParticipantCommitment` from cmd/sign/coordinator/round2.rs.
100
+ */
101
+ interface ParticipantCommitment {
102
+ commitments: Ed25519SigningCommitments;
103
+ shareArid: ARID;
104
+ }
105
+
106
+ /**
107
+ * State loaded from commitments.json.
108
+ *
109
+ * Port of `struct CommitmentsState` from cmd/sign/coordinator/round2.rs.
110
+ */
111
+ interface CommitmentsState {
112
+ commitments: Map<string, ParticipantCommitment>; // XID UR string -> commitment
113
+ }
114
+
115
+ /**
116
+ * Validate envelope and extract signature share data (for parallel fetch).
117
+ *
118
+ * Port of `validate_and_extract_share_response()` from cmd/sign/coordinator/round2.rs.
119
+ */
120
+ function validateAndExtractShareResponse(
121
+ envelope: Envelope,
122
+ _coordinatorKeys: PrivateKeys,
123
+ expectedSender: XID,
124
+ expectedSessionId: ARID,
125
+ ): SignRound2ResponseData | { rejected: string } {
126
+ // In the full implementation, we would decrypt the sealed response here
127
+ // For now, we extract the data from the envelope directly
128
+
129
+ try {
130
+ // Check the response type
131
+ envelope.checkSubjectUnit();
132
+ envelope.checkType("signRound2Response");
133
+
134
+ // Extract session ID using objectsForPredicate and then extract subjects
135
+ const sessionObjects = envelope.objectsForPredicate("session");
136
+ if (sessionObjects.length === 0) {
137
+ return { rejected: "Missing session in response" };
138
+ }
139
+ const responseSession = ARIDClass.fromTaggedCbor(sessionObjects[0].subject().tryLeaf());
140
+ if (responseSession.urString() !== expectedSessionId.urString()) {
141
+ return {
142
+ rejected: `Response session ${responseSession.urString()} does not match expected ${expectedSessionId.urString()}`,
143
+ };
144
+ }
145
+
146
+ // Extract participant XID (sender check)
147
+ const participantObjects = envelope.objectsForPredicate("participant");
148
+ if (participantObjects.length === 0) {
149
+ return { rejected: "Missing participant in response" };
150
+ }
151
+ const participantXid = XIDClass.fromTaggedCbor(participantObjects[0].subject().tryLeaf());
152
+ if (participantXid.urString() !== expectedSender.urString()) {
153
+ return {
154
+ rejected: `Unexpected response sender: ${participantXid.urString()} (expected ${expectedSender.urString()})`,
155
+ };
156
+ }
157
+
158
+ // Extract signature share (JSON-serialized)
159
+ const shareObjects = envelope.objectsForPredicate("signature_share");
160
+ if (shareObjects.length === 0) {
161
+ return { rejected: "Missing signature_share in response" };
162
+ }
163
+ const signatureShareJson = JSONComponent.fromTaggedCbor(shareObjects[0].subject().tryLeaf());
164
+ const signatureShareData = JSON.parse(signatureShareJson.toString()) as { share: string };
165
+ const signatureShare = deserializeSignatureShare(signatureShareData.share);
166
+
167
+ // Extract finalize ARID (response_arid)
168
+ const responseAridObjects = envelope.objectsForPredicate("response_arid");
169
+ if (responseAridObjects.length === 0) {
170
+ return { rejected: "Missing response_arid in response" };
171
+ }
172
+ const finalizeArid = ARIDClass.fromTaggedCbor(responseAridObjects[0].subject().tryLeaf());
173
+
174
+ return { signatureShare, finalizeArid };
175
+ } catch (error) {
176
+ const message = error instanceof Error ? error.message : String(error);
177
+ return { rejected: `Failed to parse response: ${message}` };
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Collect signature shares in parallel with progress display.
183
+ *
184
+ * Port of `collect_shares_parallel()` from cmd/sign/coordinator/round2.rs.
185
+ */
186
+ async function collectSharesParallel(
187
+ client: StorageClient,
188
+ registry: Registry,
189
+ commitmentsState: CommitmentsState,
190
+ coordinator: XIDDocument,
191
+ sessionId: ARID,
192
+ timeoutSeconds?: number,
193
+ ): Promise<CollectionResult<SignRound2ResponseData>> {
194
+ // Build requests from commitments
195
+ const requests: [XID, ARID, string][] = [];
196
+
197
+ for (const [xidUr, entry] of commitmentsState.commitments) {
198
+ const xid = XIDClass.fromURString(xidUr);
199
+ const participant = registry.participant(xid);
200
+ const name = participant?.petName() ?? xid.urString();
201
+ requests.push([xid, entry.shareArid, name]);
202
+ }
203
+
204
+ const coordinatorKeys = coordinator.inceptionPrivateKeys();
205
+ if (!coordinatorKeys) {
206
+ throw new Error("Coordinator XID document has no inception private keys");
207
+ }
208
+
209
+ const session = sessionId;
210
+
211
+ return parallelFetch(
212
+ client,
213
+ requests,
214
+ (envelope: Envelope, xid: XID) => {
215
+ return validateAndExtractShareResponse(envelope, coordinatorKeys, xid, session);
216
+ },
217
+ {
218
+ timeoutSeconds,
219
+ verbose: false,
220
+ },
221
+ );
222
+ }
223
+
224
+ /**
225
+ * Build a finalize event containing all signature shares.
226
+ *
227
+ * Port of `build_finalize_event()` from cmd/sign/coordinator/round2.rs.
228
+ */
229
+ function buildFinalizeEvent(
230
+ _sender: XIDDocument,
231
+ sessionId: ARID,
232
+ signatureSharesByXid: Map<string, Ed25519SignatureShare>,
233
+ ): SignFinalizeContent {
234
+ // Build the content with session and all signature shares
235
+ let content = SignFinalizeContent.new().addAssertion("session", sessionId);
236
+
237
+ for (const [xidUr, share] of signatureSharesByXid) {
238
+ const xid = XIDClass.fromURString(xidUr);
239
+ const shareHex = serializeSignatureShare(share);
240
+ const shareJson = JSONComponent.fromString(JSON.stringify({ share: shareHex }));
241
+ const entry = Envelope.new(xid).addAssertion("share", shareJson);
242
+ content = content.addAssertion("signature_share", entry);
243
+ }
244
+
245
+ return content;
246
+ }
247
+
248
+ /**
249
+ * Aggregate signature shares and verify the result.
250
+ *
251
+ * Port of signature aggregation logic from cmd/sign/coordinator/round2.rs.
252
+ */
253
+ function aggregateAndVerifySignature(
254
+ signingCommitments: Map<FrostIdentifier, Ed25519SigningCommitments>,
255
+ signatureSharesByIdentifier: Map<FrostIdentifier, Ed25519SignatureShare>,
256
+ publicKeyPackage: FrostPublicKeyPackage,
257
+ targetDigest: Uint8Array,
258
+ ): { signature: Signature; signatureUr: string } {
259
+ // Create signing package
260
+ const signingPackage = createSigningPackage(signingCommitments, targetDigest);
261
+
262
+ // Aggregate signature shares
263
+ const aggregatedSignature = aggregateSignatures(
264
+ signingPackage,
265
+ signatureSharesByIdentifier,
266
+ publicKeyPackage,
267
+ );
268
+
269
+ // Serialize the aggregated signature
270
+ const signatureBytes = serializeSignature(aggregatedSignature);
271
+
272
+ // Verify the signature is 64 bytes
273
+ if (signatureBytes.length !== 64) {
274
+ throw new Error("Aggregated signature is not 64 bytes");
275
+ }
276
+
277
+ // Create bc-components Signature
278
+ const signature = Signature.ed25519FromData(signatureBytes);
279
+ const signatureUr = signature.urString();
280
+
281
+ return { signature, signatureUr };
282
+ }
283
+
284
+ /**
285
+ * Persist final signing state to disk.
286
+ *
287
+ * Port of `persist_final_state()` from cmd/sign/coordinator/round2.rs.
288
+ */
289
+ function persistSigningState(
290
+ registryPath: string,
291
+ groupId: ARID,
292
+ sessionId: ARID,
293
+ signature: Signature,
294
+ signatureSharesByXid: Map<string, Ed25519SignatureShare>,
295
+ finalizeArids: Map<string, ARID>,
296
+ ): void {
297
+ const dir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
298
+ fs.mkdirSync(dir, { recursive: true });
299
+
300
+ // Build signature shares JSON object
301
+ const sharesJson: Record<string, unknown> = {};
302
+ for (const [xidUr, share] of signatureSharesByXid) {
303
+ sharesJson[xidUr] = { share: serializeSignatureShare(share) };
304
+ }
305
+
306
+ // Build finalize ARIDs JSON object
307
+ const finalizeJson: Record<string, string> = {};
308
+ for (const [xidUr, arid] of finalizeArids) {
309
+ finalizeJson[xidUr] = arid.urString();
310
+ }
311
+
312
+ // Build root JSON object
313
+ const root = {
314
+ group: groupId.urString(),
315
+ session: sessionId.urString(),
316
+ signature: signature.urString(),
317
+ signature_shares: sharesJson,
318
+ finalize_arids: finalizeJson,
319
+ };
320
+
321
+ fs.writeFileSync(path.join(dir, "final.json"), JSON.stringify(root, null, 2));
322
+ }
323
+
324
+ /**
325
+ * Load start state from disk.
326
+ *
327
+ * Port of `load_start_state()` from cmd/sign/coordinator/round2.rs.
328
+ */
329
+ function loadStartState(registryPath: string, sessionId: ARID, groupHint?: ARID): StartState {
330
+ const base = path.dirname(registryPath);
331
+ const groupStateDir = path.join(base, "group-state");
332
+
333
+ // Find candidate paths
334
+ const candidatePaths: [ARID, string][] = [];
335
+ let groupDirs: [ARID, string][];
336
+
337
+ if (groupHint) {
338
+ groupDirs = [[groupHint, path.join(groupStateDir, groupHint.hex())]];
339
+ } else {
340
+ groupDirs = [];
341
+ if (fs.existsSync(groupStateDir)) {
342
+ for (const entry of fs.readdirSync(groupStateDir, { withFileTypes: true })) {
343
+ if (entry.isDirectory() && entry.name.length === 64 && /^[0-9a-f]+$/i.test(entry.name)) {
344
+ const groupId = ARIDClass.fromHex(entry.name);
345
+ groupDirs.push([groupId, path.join(groupStateDir, entry.name)]);
346
+ }
347
+ }
348
+ }
349
+ }
350
+
351
+ for (const [groupId, groupDir] of groupDirs) {
352
+ const candidate = path.join(groupDir, "signing", sessionId.hex(), "start.json");
353
+ if (fs.existsSync(candidate)) {
354
+ candidatePaths.push([groupId, candidate]);
355
+ }
356
+ }
357
+
358
+ if (candidatePaths.length === 0) {
359
+ throw new Error("No sign start state found; run `frost sign coordinator start` first");
360
+ }
361
+ if (candidatePaths.length > 1) {
362
+ throw new Error("Multiple signing sessions found; specify --group to disambiguate");
363
+ }
364
+
365
+ const [groupId, statePath] = candidatePaths[0];
366
+ const raw = JSON.parse(fs.readFileSync(statePath, "utf-8")) as Record<string, unknown>;
367
+
368
+ const getStr = (key: string): string => {
369
+ const value = raw[key];
370
+ if (typeof value !== "string") {
371
+ throw new Error(`Missing or invalid ${key} in start.json`);
372
+ }
373
+ return value;
374
+ };
375
+
376
+ const sessionInState = parseAridUr(getStr("session_id"));
377
+ const groupInState = parseAridUr(getStr("group"));
378
+
379
+ if (sessionInState.urString() !== sessionId.urString()) {
380
+ throw new Error(
381
+ `start.json session ${sessionInState.urString()} does not match requested session ${sessionId.urString()}`,
382
+ );
383
+ }
384
+ if (groupInState.urString() !== groupId.urString()) {
385
+ throw new Error(
386
+ `start.json group ${groupInState.urString()} does not match directory group ${groupId.urString()}`,
387
+ );
388
+ }
389
+
390
+ const minSigners = raw["min_signers"];
391
+ if (typeof minSigners !== "number") {
392
+ throw new Error("Missing min_signers in start.json");
393
+ }
394
+
395
+ const participantsVal = raw["participants"] as Record<string, unknown> | undefined;
396
+ if (!participantsVal || typeof participantsVal !== "object") {
397
+ throw new Error("Missing participants in start.json");
398
+ }
399
+
400
+ const participants: XID[] = [];
401
+ for (const xidStr of Object.keys(participantsVal)) {
402
+ participants.push(XIDClass.fromURString(xidStr));
403
+ }
404
+ participants.sort((a, b) => a.urString().localeCompare(b.urString()));
405
+
406
+ const targetUr = getStr("target");
407
+
408
+ return { groupId, minSigners, participants, targetUr };
409
+ }
410
+
411
+ /**
412
+ * Load commitments state from disk.
413
+ *
414
+ * Port of `load_commitments_state()` from cmd/sign/coordinator/round2.rs.
415
+ */
416
+ function loadCommitmentsState(
417
+ registryPath: string,
418
+ groupId: ARID,
419
+ sessionId: ARID,
420
+ ): CommitmentsState {
421
+ const dir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
422
+ const statePath = path.join(dir, "commitments.json");
423
+
424
+ if (!fs.existsSync(statePath)) {
425
+ throw new Error(
426
+ `Commitments not found at ${statePath}. Run \`frost sign coordinator collect\` first`,
427
+ );
428
+ }
429
+
430
+ const raw = JSON.parse(fs.readFileSync(statePath, "utf-8")) as Record<string, unknown>;
431
+
432
+ const getStr = (key: string): string => {
433
+ const value = raw[key];
434
+ if (typeof value !== "string") {
435
+ throw new Error(`Missing or invalid ${key} in commitments.json`);
436
+ }
437
+ return value;
438
+ };
439
+
440
+ const sessionInState = parseAridUr(getStr("session"));
441
+ if (sessionInState.urString() !== sessionId.urString()) {
442
+ throw new Error(
443
+ `commitments.json session ${sessionInState.urString()} does not match requested session ${sessionId.urString()}`,
444
+ );
445
+ }
446
+
447
+ const commitmentsVal = raw["commitments"] as Record<string, unknown> | undefined;
448
+ if (!commitmentsVal || typeof commitmentsVal !== "object") {
449
+ throw new Error("Missing commitments map in commitments.json");
450
+ }
451
+
452
+ const commitments = new Map<string, ParticipantCommitment>();
453
+
454
+ for (const [xidStr, value] of Object.entries(commitmentsVal)) {
455
+ const obj = value as Record<string, unknown>;
456
+ const commitValue = obj["commitments"] as SerializedSigningCommitments | undefined;
457
+ if (!commitValue) {
458
+ throw new Error("Missing commitments value in commitments.json");
459
+ }
460
+ const commitmentsDeserialized = deserializeSigningCommitments(commitValue);
461
+
462
+ const shareAridRaw = obj["share_arid"];
463
+ if (typeof shareAridRaw !== "string") {
464
+ throw new Error("Missing share_arid in commitments.json");
465
+ }
466
+ const shareArid = parseAridUr(shareAridRaw);
467
+
468
+ commitments.set(xidStr, {
469
+ commitments: commitmentsDeserialized,
470
+ shareArid,
471
+ });
472
+ }
473
+
474
+ return { commitments };
475
+ }
476
+
477
+ /**
478
+ * Load public key package from collected_finalize.json.
479
+ *
480
+ * Port of `load_public_key_package()` from cmd/sign/coordinator/round2.rs.
481
+ */
482
+ function loadPublicKeyPackage(registryPath: string, groupId: ARID): FrostPublicKeyPackage {
483
+ const base = path.dirname(registryPath);
484
+ const pkgPath = path.join(base, "group-state", groupId.hex(), "collected_finalize.json");
485
+
486
+ if (!fs.existsSync(pkgPath)) {
487
+ throw new Error(
488
+ `collected_finalize.json not found at ${pkgPath}. Run \`frost dkg coordinator finalize collect\` first`,
489
+ );
490
+ }
491
+
492
+ const raw = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as Record<string, unknown>;
493
+ const firstEntry = Object.values(raw)[0] as Record<string, unknown> | undefined;
494
+
495
+ if (!firstEntry) {
496
+ throw new Error("collected_finalize.json is empty");
497
+ }
498
+
499
+ const publicKeyValue = firstEntry["public_key_package"] as SerializedPublicKeyPackage | undefined;
500
+ if (!publicKeyValue) {
501
+ throw new Error("public_key_package missing in collected_finalize.json");
502
+ }
503
+
504
+ return deserializePublicKeyPackage(publicKeyValue);
505
+ }
506
+
507
+ /**
508
+ * Build a map from XID to FROST identifier.
509
+ *
510
+ * Port of `xid_identifier_map()` from cmd/sign/coordinator/round2.rs.
511
+ */
512
+ function xidIdentifierMap(participants: XID[]): Map<string, FrostIdentifier> {
513
+ const map = new Map<string, FrostIdentifier>();
514
+ for (let i = 0; i < participants.length; i++) {
515
+ const identifier = identifierFromU16(i + 1);
516
+ map.set(participants[i].urString(), identifier);
517
+ }
518
+ return map;
519
+ }
520
+
521
+ /**
522
+ * Build signing commitments with identifiers.
523
+ *
524
+ * Port of `commitments_with_identifiers()` from cmd/sign/coordinator/round2.rs.
525
+ */
526
+ function commitmentsWithIdentifiers(
527
+ commitments: Map<string, ParticipantCommitment>,
528
+ xidToIdentifier: Map<string, FrostIdentifier>,
529
+ ): Map<FrostIdentifier, Ed25519SigningCommitments> {
530
+ const mapped = new Map<FrostIdentifier, Ed25519SigningCommitments>();
531
+ for (const [xidUr, entry] of commitments) {
532
+ const identifier = xidToIdentifier.get(xidUr);
533
+ if (!identifier) {
534
+ throw new Error(`Unknown participant ${xidUr}`);
535
+ }
536
+ mapped.set(identifier, entry.commitments);
537
+ }
538
+ return mapped;
539
+ }
540
+
541
+ /**
542
+ * Execute the sign coordinator round 2 command.
543
+ *
544
+ * Collects signature shares, aggregates the signature, and posts finalize packages.
545
+ *
546
+ * Port of `CommandArgs::exec()` from cmd/sign/coordinator/round2.rs.
547
+ */
548
+ export async function round2(
549
+ client: StorageClient,
550
+ options: SignRound2Options,
551
+ cwd: string,
552
+ ): Promise<SignRound2Result> {
553
+ const registryPath = resolveRegistryPath(options.registryPath, cwd);
554
+ const registry = Registry.load(registryPath);
555
+
556
+ const owner = registry.owner();
557
+ if (!owner) {
558
+ throw new Error("Registry owner is required");
559
+ }
560
+
561
+ const sessionId = parseAridUr(options.sessionId);
562
+ const groupHint = options.groupId ? parseAridUr(options.groupId) : undefined;
563
+
564
+ // Load start state (finds group automatically if not specified)
565
+ const startState = loadStartState(registryPath, sessionId, groupHint);
566
+ const groupId = startState.groupId;
567
+
568
+ const groupRecord = registry.group(groupId);
569
+ if (!groupRecord) {
570
+ throw new Error("Group not found in registry");
571
+ }
572
+
573
+ // Verify coordinator ownership
574
+ if (groupRecord.coordinator().xid().urString() !== owner.xid().urString()) {
575
+ throw new Error(
576
+ `Only the coordinator can finalize signing. Coordinator: ${groupRecord.coordinator().xid().urString()}, Owner: ${owner.xid().urString()}`,
577
+ );
578
+ }
579
+
580
+ // Load commitments state
581
+ const commitmentsState = loadCommitmentsState(registryPath, groupId, sessionId);
582
+
583
+ // Build XID to identifier map
584
+ const xidToIdentifier = xidIdentifierMap(startState.participants);
585
+
586
+ // Collect signature shares
587
+ let signatureSharesByIdentifier: Map<FrostIdentifier, Ed25519SignatureShare>;
588
+ let signatureSharesByXid: Map<string, Ed25519SignatureShare>;
589
+ let finalizeArids: Map<string, ARID>;
590
+
591
+ if (options.parallel === true) {
592
+ // Parallel collection path
593
+ const collection = await collectSharesParallel(
594
+ client,
595
+ registry,
596
+ commitmentsState,
597
+ owner.xidDocument(),
598
+ sessionId,
599
+ options.timeoutSeconds,
600
+ );
601
+
602
+ if (!collection.allSucceeded()) {
603
+ // Report failures
604
+ if (collection.rejections.length > 0) {
605
+ console.error("\nRejections:");
606
+ for (const [xid, reason] of collection.rejections) {
607
+ console.error(` ${xid.urString()}: ${reason}`);
608
+ }
609
+ }
610
+ if (collection.errors.length > 0) {
611
+ console.error("\nErrors:");
612
+ for (const [xid, error] of collection.errors) {
613
+ console.error(` ${xid.urString()}: ${error}`);
614
+ }
615
+ }
616
+ if (collection.timeouts.length > 0) {
617
+ console.error("\nTimeouts:");
618
+ for (const xid of collection.timeouts) {
619
+ console.error(` ${xid.urString()}`);
620
+ }
621
+ }
622
+ throw new Error(
623
+ `Signature share collection incomplete: ${collection.successes.length} succeeded, ` +
624
+ `${collection.rejections.length} rejected, ${collection.errors.length} errors, ` +
625
+ `${collection.timeouts.length} timeouts`,
626
+ );
627
+ }
628
+
629
+ // Convert collection to maps
630
+ signatureSharesByIdentifier = new Map();
631
+ signatureSharesByXid = new Map();
632
+ finalizeArids = new Map();
633
+
634
+ for (const [xid, data] of collection.successes) {
635
+ const xidUr = xid.urString();
636
+ const identifier = xidToIdentifier.get(xidUr);
637
+ if (!identifier) {
638
+ throw new Error("Identifier mapping missing for participant");
639
+ }
640
+ signatureSharesByIdentifier.set(identifier, data.signatureShare);
641
+ signatureSharesByXid.set(xidUr, data.signatureShare);
642
+ finalizeArids.set(xidUr, data.finalizeArid);
643
+ }
644
+ } else {
645
+ // Sequential collection path
646
+ if (options.verbose === true) {
647
+ console.error(
648
+ `Collecting signature shares for session ${sessionId.urString()} from ${commitmentsState.commitments.size} participants...`,
649
+ );
650
+ }
651
+
652
+ signatureSharesByIdentifier = new Map();
653
+ signatureSharesByXid = new Map();
654
+ finalizeArids = new Map();
655
+
656
+ for (const [xidUr, entry] of commitmentsState.commitments) {
657
+ const xid = XIDClass.fromURString(xidUr);
658
+ const participant = registry.participant(xid);
659
+ const participantName = participant?.petName() ?? xid.urString();
660
+
661
+ const identifier = xidToIdentifier.get(xidUr);
662
+ if (!identifier) {
663
+ throw new Error("Identifier mapping missing for participant");
664
+ }
665
+
666
+ // Fetch the response
667
+ const envelope = await client.get(entry.shareArid, options.timeoutSeconds);
668
+ if (!envelope) {
669
+ throw new Error(`Signature share response not found for ${participantName}`);
670
+ }
671
+
672
+ const coordinatorKeys = owner.xidDocument().inceptionPrivateKeys();
673
+ if (!coordinatorKeys) {
674
+ throw new Error("Coordinator XID document has no inception private keys");
675
+ }
676
+
677
+ const result = validateAndExtractShareResponse(envelope, coordinatorKeys, xid, sessionId);
678
+ if ("rejected" in result) {
679
+ throw new Error(`Participant rejected signRound2: ${result.rejected}`);
680
+ }
681
+
682
+ signatureSharesByIdentifier.set(identifier, result.signatureShare);
683
+ signatureSharesByXid.set(xidUr, result.signatureShare);
684
+ finalizeArids.set(xidUr, result.finalizeArid);
685
+ }
686
+ }
687
+
688
+ // Verify we have enough shares
689
+ if (signatureSharesByIdentifier.size < startState.minSigners) {
690
+ throw new Error(
691
+ `Only collected ${signatureSharesByIdentifier.size} signature shares, need at least ${startState.minSigners}`,
692
+ );
693
+ }
694
+
695
+ // Build signing commitments with identifiers
696
+ const signingCommitments = commitmentsWithIdentifiers(
697
+ commitmentsState.commitments,
698
+ xidToIdentifier,
699
+ );
700
+
701
+ // Get target digest
702
+ const targetEnvelope = Envelope.fromURString(startState.targetUr);
703
+ const targetDigest = targetEnvelope.subject().digest().data();
704
+
705
+ // Load public key package
706
+ const publicKeyPackage = loadPublicKeyPackage(registryPath, groupId);
707
+ const verifyingKey = signingKeyFromVerifying(publicKeyPackage.verifyingKey);
708
+
709
+ // Aggregate and verify signature
710
+ const { signature, signatureUr } = aggregateAndVerifySignature(
711
+ signingCommitments,
712
+ signatureSharesByIdentifier,
713
+ publicKeyPackage,
714
+ targetDigest,
715
+ );
716
+
717
+ // Verify signature against target digest
718
+ // @ts-expect-error - verifyingKey type mismatch
719
+ if (verifyingKey.verify(signature, targetDigest) !== true) {
720
+ throw new Error("Aggregated signature failed verification against target digest");
721
+ }
722
+
723
+ // Attach signature to target and verify
724
+
725
+ const signedEnvelope = Envelope.fromURString(startState.targetUr).addAssertion(
726
+ "signed",
727
+ signature,
728
+ );
729
+ const signedEnvelopeUr = signedEnvelope.urString();
730
+
731
+ // Persist final state
732
+ persistSigningState(
733
+ registryPath,
734
+ groupId,
735
+ sessionId,
736
+ signature,
737
+ signatureSharesByXid,
738
+ finalizeArids,
739
+ );
740
+
741
+ if (options.verbose === true) {
742
+ console.error();
743
+ console.error(
744
+ `Aggregated signature for session ${sessionId.urString()} and prepared ${finalizeArids.size} finalize packages.`,
745
+ );
746
+ console.error("Signature verified against target and group key.");
747
+ }
748
+
749
+ // Dispatch finalize events to participants
750
+ const signerKeys = owner.xidDocument().inceptionPrivateKeys();
751
+ if (!signerKeys) {
752
+ throw new Error("Coordinator XID document has no signing keys");
753
+ }
754
+
755
+ if (options.verbose === true) {
756
+ console.error(`Dispatching finalize packages to ${finalizeArids.size} participants...`);
757
+ } else {
758
+ // Blank line to separate get phase from put phase
759
+ console.error();
760
+ }
761
+
762
+ // Build finalize messages
763
+ const messages: [XID, ARID, Envelope, string][] = [];
764
+ let previewPrinted = false;
765
+
766
+ for (const [xidUr, finalizeArid] of finalizeArids) {
767
+ const participantXid = XIDClass.fromURString(xidUr);
768
+ const participant = registry.participant(participantXid);
769
+ const participantName = participant?.petName() ?? xidUr;
770
+
771
+ const recipientDoc =
772
+ xidUr === owner.xid().urString() ? owner.xidDocument() : participant?.xidDocument();
773
+
774
+ if (!recipientDoc) {
775
+ throw new Error(`Participant ${xidUr} not found in registry`);
776
+ }
777
+
778
+ const event = buildFinalizeEvent(owner.xidDocument(), sessionId, signatureSharesByXid);
779
+
780
+ if (options.previewFinalize === true && !previewPrinted) {
781
+ // Preview as unsigned, unencrypted envelope
782
+ console.log(`# signFinalize preview for ${participantXid.urString()}`);
783
+ console.log(event.envelope().format());
784
+ previewPrinted = true;
785
+ }
786
+
787
+ // For now, use the plain envelope (GSTP sealing would be applied in full implementation)
788
+ const sealed = event.envelope();
789
+
790
+ messages.push([participantXid, finalizeArid, sealed, participantName]);
791
+ }
792
+
793
+ // Dispatch messages
794
+ if (options.parallel === true) {
795
+ // Parallel send
796
+ console.error();
797
+ const results = await parallelSend(client, messages, options.verbose === true);
798
+
799
+ // Check for errors
800
+ const errors: string[] = [];
801
+ for (const [xid, result] of results) {
802
+ if (result !== null) {
803
+ const participant = registry.participant(xid);
804
+ const name = participant?.petName() ?? xid.urString();
805
+ errors.push(`${name}: ${result.message}`);
806
+ }
807
+ }
808
+ if (errors.length > 0) {
809
+ throw new Error(`Failed to send finalize packages: ${errors.join("; ")}`);
810
+ }
811
+ } else {
812
+ // Sequential send
813
+ for (const [_xid, finalizeArid, sealed, participantName] of messages) {
814
+ await putWithIndicator(
815
+ client,
816
+ finalizeArid,
817
+ sealed,
818
+ participantName,
819
+ options.verbose ?? false,
820
+ );
821
+ }
822
+ }
823
+
824
+ // Print final signature and signed envelope UR
825
+ console.log(signatureUr);
826
+ console.log(signedEnvelopeUr);
827
+
828
+ return {
829
+ signature: signatureUr,
830
+ signedEnvelope: signedEnvelopeUr,
831
+ accepted: signatureSharesByIdentifier.size,
832
+ rejected: 0,
833
+ errors: 0,
834
+ timeouts: 0,
835
+ };
836
+ }