@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.
- package/LICENSE +48 -0
- package/README.md +35 -0
- package/dist/bin/frost.cjs +109 -0
- package/dist/bin/frost.cjs.map +1 -0
- package/dist/bin/frost.d.cts +1 -0
- package/dist/bin/frost.d.mts +1 -0
- package/dist/bin/frost.mjs +109 -0
- package/dist/bin/frost.mjs.map +1 -0
- package/dist/chunk-CQwRTUmo.cjs +53 -0
- package/dist/chunk-D3JzZLW2.mjs +21 -0
- package/dist/cmd/index.cjs +45 -0
- package/dist/cmd/index.d.cts +4 -0
- package/dist/cmd/index.d.mts +4 -0
- package/dist/cmd/index.mjs +7 -0
- package/dist/cmd-C8pmNd28.mjs +4664 -0
- package/dist/cmd-C8pmNd28.mjs.map +1 -0
- package/dist/cmd-CxUgryx_.cjs +4803 -0
- package/dist/cmd-CxUgryx_.cjs.map +1 -0
- package/dist/dkg/index.cjs +7 -0
- package/dist/dkg/index.d.cts +2 -0
- package/dist/dkg/index.d.mts +2 -0
- package/dist/dkg/index.mjs +3 -0
- package/dist/dkg-D4RcblWl.cjs +364 -0
- package/dist/dkg-D4RcblWl.cjs.map +1 -0
- package/dist/dkg-DqGrAV81.mjs +334 -0
- package/dist/dkg-DqGrAV81.mjs.map +1 -0
- package/dist/frost/index.cjs +37 -0
- package/dist/frost/index.d.cts +207 -0
- package/dist/frost/index.d.cts.map +1 -0
- package/dist/frost/index.d.mts +207 -0
- package/dist/frost/index.d.mts.map +1 -0
- package/dist/frost/index.mjs +3 -0
- package/dist/frost-CMH1K0Cw.cjs +511 -0
- package/dist/frost-CMH1K0Cw.cjs.map +1 -0
- package/dist/frost-Csp0IOrd.mjs +326 -0
- package/dist/frost-Csp0IOrd.mjs.map +1 -0
- package/dist/index-BGVoWW5P.d.cts +172 -0
- package/dist/index-BGVoWW5P.d.cts.map +1 -0
- package/dist/index-BJeUYrdE.d.mts +396 -0
- package/dist/index-BJeUYrdE.d.mts.map +1 -0
- package/dist/index-ByMDUYKw.d.mts +1098 -0
- package/dist/index-ByMDUYKw.d.mts.map +1 -0
- package/dist/index-DejLkr_F.d.mts +172 -0
- package/dist/index-DejLkr_F.d.mts.map +1 -0
- package/dist/index-Dib1OE-e.d.cts +1098 -0
- package/dist/index-Dib1OE-e.d.cts.map +1 -0
- package/dist/index-DnvBKgec.d.cts +396 -0
- package/dist/index-DnvBKgec.d.cts.map +1 -0
- package/dist/index.cjs +85 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +15 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +15 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +24 -0
- package/dist/index.mjs.map +1 -0
- package/dist/registry/index.cjs +13 -0
- package/dist/registry/index.d.cts +2 -0
- package/dist/registry/index.d.mts +2 -0
- package/dist/registry/index.mjs +3 -0
- package/dist/registry-CBjRRqNv.mjs +144 -0
- package/dist/registry-CBjRRqNv.mjs.map +1 -0
- package/dist/registry-CWp2amuo.mjs +789 -0
- package/dist/registry-CWp2amuo.mjs.map +1 -0
- package/dist/registry-D5yh293y.cjs +857 -0
- package/dist/registry-D5yh293y.cjs.map +1 -0
- package/dist/registry-DNUNW6SH.cjs +163 -0
- package/dist/registry-DNUNW6SH.cjs.map +1 -0
- package/package.json +119 -0
- package/src/bin/frost.ts +218 -0
- package/src/cmd/busy.ts +64 -0
- package/src/cmd/check.ts +20 -0
- package/src/cmd/common.ts +40 -0
- package/src/cmd/dkg/common.ts +275 -0
- package/src/cmd/dkg/coordinator/finalize.ts +592 -0
- package/src/cmd/dkg/coordinator/index.ts +12 -0
- package/src/cmd/dkg/coordinator/invite.ts +217 -0
- package/src/cmd/dkg/coordinator/round1.ts +889 -0
- package/src/cmd/dkg/coordinator/round2.ts +959 -0
- package/src/cmd/dkg/index.ts +11 -0
- package/src/cmd/dkg/participant/finalize.ts +575 -0
- package/src/cmd/dkg/participant/index.ts +12 -0
- package/src/cmd/dkg/participant/receive.ts +348 -0
- package/src/cmd/dkg/participant/round1.ts +464 -0
- package/src/cmd/dkg/participant/round2.ts +627 -0
- package/src/cmd/index.ts +18 -0
- package/src/cmd/parallel.ts +334 -0
- package/src/cmd/registry/index.ts +88 -0
- package/src/cmd/registry/owner/index.ts +9 -0
- package/src/cmd/registry/owner/set.ts +70 -0
- package/src/cmd/registry/participant/add.ts +70 -0
- package/src/cmd/registry/participant/index.ts +9 -0
- package/src/cmd/sign/common.ts +108 -0
- package/src/cmd/sign/coordinator/index.ts +11 -0
- package/src/cmd/sign/coordinator/invite.ts +431 -0
- package/src/cmd/sign/coordinator/round1.ts +751 -0
- package/src/cmd/sign/coordinator/round2.ts +836 -0
- package/src/cmd/sign/index.ts +11 -0
- package/src/cmd/sign/participant/finalize.ts +823 -0
- package/src/cmd/sign/participant/index.ts +12 -0
- package/src/cmd/sign/participant/receive.ts +378 -0
- package/src/cmd/sign/participant/round1.ts +479 -0
- package/src/cmd/sign/participant/round2.ts +748 -0
- package/src/cmd/storage.ts +116 -0
- package/src/dkg/group-invite.ts +414 -0
- package/src/dkg/index.ts +10 -0
- package/src/dkg/proposed-participant.ts +132 -0
- package/src/frost/index.ts +456 -0
- package/src/index.ts +45 -0
- package/src/registry/group-record.ts +392 -0
- package/src/registry/index.ts +12 -0
- package/src/registry/owner-record.ts +146 -0
- package/src/registry/participant-record.ts +186 -0
- package/src/registry/registry-impl.ts +364 -0
|
@@ -0,0 +1,4664 @@
|
|
|
1
|
+
import { n as __require, t as __exportAll } from "./chunk-D3JzZLW2.mjs";
|
|
2
|
+
import { n as DkgInvite, t as DkgInvitation } from "./dkg-DqGrAV81.mjs";
|
|
3
|
+
import { a as resolveRegistryPath, c as ContributionPaths, d as PendingRequests, i as Registry, l as GroupParticipant, u as GroupRecord } from "./registry-CWp2amuo.mjs";
|
|
4
|
+
import { A as serializeSignatureShare, D as serializePublicKeyPackage, E as serializeKeyPackage, M as serializeSigningNonces, N as signingRound1, O as serializeSignature, P as signingRound2, _ as dkgPart3, a as bytesToHex, b as identifierFromU16, d as deserializeKeyPackage, f as deserializePublicKeyPackage, g as dkgPart2, h as dkgPart1, i as aggregateSignatures, j as serializeSigningCommitments, m as deserializeSigningCommitments, o as createRng, p as deserializeSignatureShare, s as createSigningPackage, w as serializeDkgRound2Package, x as identifierToHex, y as hexToBytes$1 } from "./frost-Csp0IOrd.mjs";
|
|
5
|
+
import { ARID, JSON as JSON$1, Signature, XID } from "@bcts/components";
|
|
6
|
+
import { CborDate } from "@bcts/dcbor";
|
|
7
|
+
import { Envelope, Function } from "@bcts/envelope";
|
|
8
|
+
import { SealedEvent, SealedRequest, SealedResponse } from "@bcts/gstp";
|
|
9
|
+
import { XIDDocument, XIDVerifySignature } from "@bcts/xid";
|
|
10
|
+
import { UR } from "@bcts/uniform-resources";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { Ed25519Sha512, serde } from "@frosts/ed25519";
|
|
14
|
+
import { CoefficientCommitment, Nonce, SigningNonces, VerifiableSecretSharingCommitment, round1, round2 } from "@frosts/core";
|
|
15
|
+
|
|
16
|
+
//#region src/cmd/common.ts
|
|
17
|
+
/**
|
|
18
|
+
* Common utilities for commands.
|
|
19
|
+
*
|
|
20
|
+
* Port of cmd/common.rs from frost-hubert-rust.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Get the group state directory for a given registry path and group ID.
|
|
26
|
+
*
|
|
27
|
+
* Port of `group_state_dir()` from cmd/common.rs.
|
|
28
|
+
*/
|
|
29
|
+
function groupStateDir(registryPath, groupIdHex) {
|
|
30
|
+
const base = path.dirname(registryPath);
|
|
31
|
+
return path.join(base, "group-state", groupIdHex);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Global verbose flag.
|
|
35
|
+
*/
|
|
36
|
+
let verboseFlag = false;
|
|
37
|
+
/**
|
|
38
|
+
* Set the verbose flag.
|
|
39
|
+
*/
|
|
40
|
+
function setVerbose(value) {
|
|
41
|
+
verboseFlag = value;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if verbose mode is enabled.
|
|
45
|
+
*
|
|
46
|
+
* Port of `is_verbose()` from cmd/common.rs.
|
|
47
|
+
*/
|
|
48
|
+
function isVerbose() {
|
|
49
|
+
return verboseFlag;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/cmd/busy.ts
|
|
54
|
+
/**
|
|
55
|
+
* Put an envelope to storage with a progress indicator.
|
|
56
|
+
*
|
|
57
|
+
* Port of `put_with_indicator()` from cmd/busy.rs.
|
|
58
|
+
*/
|
|
59
|
+
async function putWithIndicator(client, arid, envelope, message, verbose) {
|
|
60
|
+
if (verbose) console.log(`${message}...`);
|
|
61
|
+
await client.put(arid, envelope);
|
|
62
|
+
if (verbose) console.log(`${message}... done`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Get an envelope from storage with a progress indicator.
|
|
66
|
+
*
|
|
67
|
+
* Port of `get_with_indicator()` from cmd/busy.rs.
|
|
68
|
+
*/
|
|
69
|
+
async function getWithIndicator(client, arid, message, timeoutSeconds, verbose) {
|
|
70
|
+
if (verbose) console.log(`${message}...`);
|
|
71
|
+
const envelope = await client.get(arid, timeoutSeconds);
|
|
72
|
+
if (verbose) if (envelope) console.log(`${message}... found`);
|
|
73
|
+
else console.log(`${message}... not found`);
|
|
74
|
+
return envelope;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/cmd/parallel.ts
|
|
79
|
+
/**
|
|
80
|
+
* Create a Pending status.
|
|
81
|
+
*/
|
|
82
|
+
function fetchStatusPending() {
|
|
83
|
+
return { type: "Pending" };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create a Success status.
|
|
87
|
+
*/
|
|
88
|
+
function fetchStatusSuccess(envelope) {
|
|
89
|
+
return {
|
|
90
|
+
type: "Success",
|
|
91
|
+
envelope
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create a Rejected status.
|
|
96
|
+
*/
|
|
97
|
+
function fetchStatusRejected(reason) {
|
|
98
|
+
return {
|
|
99
|
+
type: "Rejected",
|
|
100
|
+
reason
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Create an Error status.
|
|
105
|
+
*/
|
|
106
|
+
function fetchStatusError(error) {
|
|
107
|
+
return {
|
|
108
|
+
type: "Error",
|
|
109
|
+
error
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Create a Timeout status.
|
|
114
|
+
*/
|
|
115
|
+
function fetchStatusTimeout() {
|
|
116
|
+
return { type: "Timeout" };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Direction of the operation (get or put).
|
|
120
|
+
*
|
|
121
|
+
* Port of `enum Direction` from cmd/parallel.rs.
|
|
122
|
+
*/
|
|
123
|
+
let Direction = /* @__PURE__ */ function(Direction) {
|
|
124
|
+
/** Downloading from storage */
|
|
125
|
+
Direction["Get"] = "Get";
|
|
126
|
+
/** Uploading to storage */
|
|
127
|
+
Direction["Put"] = "Put";
|
|
128
|
+
return Direction;
|
|
129
|
+
}({});
|
|
130
|
+
/**
|
|
131
|
+
* Get the emoji for a direction.
|
|
132
|
+
*
|
|
133
|
+
* Port of `Direction::emoji()` from cmd/parallel.rs.
|
|
134
|
+
*/
|
|
135
|
+
function directionEmoji(direction) {
|
|
136
|
+
switch (direction) {
|
|
137
|
+
case Direction.Get: return "⬇️";
|
|
138
|
+
case Direction.Put: return "⬆️";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Default timeout in seconds (10 minutes).
|
|
143
|
+
*/
|
|
144
|
+
const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
145
|
+
/**
|
|
146
|
+
* Create a config with the specified timeout.
|
|
147
|
+
*/
|
|
148
|
+
function parallelFetchConfigWithTimeout(timeoutSeconds) {
|
|
149
|
+
return { timeoutSeconds };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Result of collecting responses from multiple participants.
|
|
153
|
+
*
|
|
154
|
+
* Port of `struct CollectionResult` from cmd/parallel.rs.
|
|
155
|
+
*/
|
|
156
|
+
var CollectionResult = class {
|
|
157
|
+
/** Successful responses as [XID, T] tuples */
|
|
158
|
+
successes;
|
|
159
|
+
/** Participants who explicitly rejected as [XID, reason] tuples */
|
|
160
|
+
rejections;
|
|
161
|
+
/** Participants with network/parsing errors as [XID, error] tuples */
|
|
162
|
+
errors;
|
|
163
|
+
/** Participants who timed out */
|
|
164
|
+
timeouts;
|
|
165
|
+
constructor() {
|
|
166
|
+
this.successes = [];
|
|
167
|
+
this.rejections = [];
|
|
168
|
+
this.errors = [];
|
|
169
|
+
this.timeouts = [];
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Check if enough responses were received to proceed.
|
|
173
|
+
*
|
|
174
|
+
* Port of `CollectionResult::can_proceed()` from cmd/parallel.rs.
|
|
175
|
+
*/
|
|
176
|
+
canProceed(minRequired) {
|
|
177
|
+
return this.successes.length >= minRequired;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Total number of participants.
|
|
181
|
+
*
|
|
182
|
+
* Port of `CollectionResult::total()` from cmd/parallel.rs.
|
|
183
|
+
*/
|
|
184
|
+
total() {
|
|
185
|
+
return this.successes.length + this.rejections.length + this.errors.length + this.timeouts.length;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Check if all responses succeeded.
|
|
189
|
+
*
|
|
190
|
+
* Port of `CollectionResult::all_succeeded()` from cmd/parallel.rs.
|
|
191
|
+
*/
|
|
192
|
+
allSucceeded() {
|
|
193
|
+
return this.rejections.length === 0 && this.errors.length === 0 && this.timeouts.length === 0;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Create an empty collection result.
|
|
198
|
+
*/
|
|
199
|
+
function emptyCollectionResult() {
|
|
200
|
+
return new CollectionResult();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Helper to build request tuples from pending requests and registry.
|
|
204
|
+
*
|
|
205
|
+
* Port of `build_fetch_requests()` from cmd/parallel.rs.
|
|
206
|
+
*/
|
|
207
|
+
function buildFetchRequests(pending, getName) {
|
|
208
|
+
const requests = [];
|
|
209
|
+
for (const [xid, arid] of pending) {
|
|
210
|
+
const name = getName(xid);
|
|
211
|
+
requests.push([
|
|
212
|
+
xid,
|
|
213
|
+
arid,
|
|
214
|
+
name
|
|
215
|
+
]);
|
|
216
|
+
}
|
|
217
|
+
return requests;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Simple progress output for non-interactive terminals.
|
|
221
|
+
*/
|
|
222
|
+
function logProgress(direction, name, status, message) {
|
|
223
|
+
const emoji = directionEmoji(direction);
|
|
224
|
+
switch (status) {
|
|
225
|
+
case "success":
|
|
226
|
+
console.error(`${emoji} ✅ ${name}`);
|
|
227
|
+
break;
|
|
228
|
+
case "error":
|
|
229
|
+
console.error(`${emoji} ❌ ${name}: ${message ?? "Error"}`);
|
|
230
|
+
break;
|
|
231
|
+
case "timeout":
|
|
232
|
+
console.error(`${emoji} ❌ ${name}: Timeout`);
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Fetch messages from multiple ARIDs in parallel.
|
|
238
|
+
*
|
|
239
|
+
* Port of `parallel_fetch()` from cmd/parallel.rs.
|
|
240
|
+
*/
|
|
241
|
+
async function parallelFetch(client, requests, validate, config) {
|
|
242
|
+
const result = new CollectionResult();
|
|
243
|
+
const timeoutSeconds = config.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
244
|
+
if (config.verbose === true) console.error(`Waiting for ${requests.length} responses...`);
|
|
245
|
+
const promises = requests.map(async ([xid, arid, name]) => {
|
|
246
|
+
try {
|
|
247
|
+
const envelope = await client.get(arid, timeoutSeconds);
|
|
248
|
+
if (!envelope) {
|
|
249
|
+
result.timeouts.push(xid);
|
|
250
|
+
if (config.verbose === true) logProgress(Direction.Get, name, "timeout");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const parsed = validate(envelope, xid);
|
|
254
|
+
if (parsed !== null && typeof parsed === "object" && "rejected" in parsed) {
|
|
255
|
+
result.rejections.push([xid, parsed.rejected]);
|
|
256
|
+
if (config.verbose === true) logProgress(Direction.Get, name, "error", parsed.rejected);
|
|
257
|
+
} else {
|
|
258
|
+
result.successes.push([xid, parsed]);
|
|
259
|
+
if (config.verbose === true) logProgress(Direction.Get, name, "success");
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
263
|
+
if (errorMessage.includes("Timeout") || errorMessage.includes("timeout")) {
|
|
264
|
+
result.timeouts.push(xid);
|
|
265
|
+
if (config.verbose === true) logProgress(Direction.Get, name, "timeout");
|
|
266
|
+
} else {
|
|
267
|
+
result.errors.push([xid, `${name}: ${errorMessage}`]);
|
|
268
|
+
if (config.verbose === true) logProgress(Direction.Get, name, "error", errorMessage);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
await Promise.all(promises);
|
|
273
|
+
if (config.verbose === true) {
|
|
274
|
+
console.error(`Collected ${result.successes.length} responses`);
|
|
275
|
+
if (result.rejections.length > 0) console.error(` ${result.rejections.length} rejections`);
|
|
276
|
+
if (result.errors.length > 0) console.error(` ${result.errors.length} errors`);
|
|
277
|
+
if (result.timeouts.length > 0) console.error(` ${result.timeouts.length} timeouts`);
|
|
278
|
+
}
|
|
279
|
+
return result;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Send messages to multiple ARIDs in parallel.
|
|
283
|
+
*
|
|
284
|
+
* Port of `parallel_send()` from cmd/parallel.rs.
|
|
285
|
+
*/
|
|
286
|
+
async function parallelSend(client, messages, verbose) {
|
|
287
|
+
const results = [];
|
|
288
|
+
if (verbose === true) console.error(`Sending to ${messages.length} participants...`);
|
|
289
|
+
const promises = messages.map(async ([xid, arid, envelope, name]) => {
|
|
290
|
+
try {
|
|
291
|
+
await client.put(arid, envelope);
|
|
292
|
+
results.push([xid, null]);
|
|
293
|
+
if (verbose === true) logProgress(Direction.Put, name, "success");
|
|
294
|
+
} catch (error) {
|
|
295
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
296
|
+
results.push([xid, err]);
|
|
297
|
+
if (verbose === true) logProgress(Direction.Put, name, "error", err.message);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
await Promise.all(promises);
|
|
301
|
+
if (verbose === true) {
|
|
302
|
+
const successes = results.filter(([_, err]) => err === null).length;
|
|
303
|
+
const failures = results.length - successes;
|
|
304
|
+
console.error(`Sent ${successes} messages`);
|
|
305
|
+
if (failures > 0) console.error(` ${failures} failed`);
|
|
306
|
+
}
|
|
307
|
+
return results;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/cmd/storage.ts
|
|
312
|
+
/**
|
|
313
|
+
* Create a storage client based on the selection.
|
|
314
|
+
*
|
|
315
|
+
* Port of storage client creation from cmd/storage.rs.
|
|
316
|
+
*/
|
|
317
|
+
async function createStorageClient(selection, serverUrl) {
|
|
318
|
+
switch (selection) {
|
|
319
|
+
case "server": {
|
|
320
|
+
const { ServerKvClient } = await import("@bcts/hubert/server");
|
|
321
|
+
const client = new ServerKvClient(serverUrl ?? "http://localhost:8080");
|
|
322
|
+
return {
|
|
323
|
+
async put(arid, envelope) {
|
|
324
|
+
await client.put(arid, envelope);
|
|
325
|
+
},
|
|
326
|
+
async get(arid, timeoutSeconds) {
|
|
327
|
+
return await client.get(arid, timeoutSeconds) ?? void 0;
|
|
328
|
+
},
|
|
329
|
+
exists(arid) {
|
|
330
|
+
return client.exists(arid);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
case "mainline": {
|
|
335
|
+
const { MainlineDhtKv } = await import("@bcts/hubert/mainline");
|
|
336
|
+
const client = await MainlineDhtKv.create();
|
|
337
|
+
return {
|
|
338
|
+
async put(arid, envelope) {
|
|
339
|
+
await client.put(arid, envelope);
|
|
340
|
+
},
|
|
341
|
+
async get(arid, timeoutSeconds) {
|
|
342
|
+
return await client.get(arid, timeoutSeconds) ?? void 0;
|
|
343
|
+
},
|
|
344
|
+
exists(arid) {
|
|
345
|
+
return client.exists(arid);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
case "ipfs": {
|
|
350
|
+
const { IpfsKv } = await import("@bcts/hubert/ipfs");
|
|
351
|
+
const client = new IpfsKv("http://127.0.0.1:5001");
|
|
352
|
+
return {
|
|
353
|
+
async put(arid, envelope) {
|
|
354
|
+
await client.put(arid, envelope);
|
|
355
|
+
},
|
|
356
|
+
async get(arid, timeoutSeconds) {
|
|
357
|
+
return await client.get(arid, timeoutSeconds) ?? void 0;
|
|
358
|
+
},
|
|
359
|
+
exists(arid) {
|
|
360
|
+
return client.exists(arid);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
case "hybrid": {
|
|
365
|
+
const { HybridKv } = await import("@bcts/hubert/hybrid");
|
|
366
|
+
const client = await HybridKv.create("http://127.0.0.1:5001");
|
|
367
|
+
return {
|
|
368
|
+
async put(arid, envelope) {
|
|
369
|
+
await client.put(arid, envelope);
|
|
370
|
+
},
|
|
371
|
+
async get(arid, timeoutSeconds) {
|
|
372
|
+
return await client.get(arid, timeoutSeconds) ?? void 0;
|
|
373
|
+
},
|
|
374
|
+
exists(arid) {
|
|
375
|
+
return client.exists(arid);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region src/cmd/check.ts
|
|
384
|
+
/**
|
|
385
|
+
* Check if an ARID exists in storage.
|
|
386
|
+
*
|
|
387
|
+
* Port of check functionality from cmd/check.rs.
|
|
388
|
+
*/
|
|
389
|
+
async function checkAridExists(client, arid) {
|
|
390
|
+
return client.exists(arid);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/cmd/dkg/common.ts
|
|
395
|
+
/**
|
|
396
|
+
* Common utilities for DKG commands.
|
|
397
|
+
*
|
|
398
|
+
* Port of cmd/dkg/common.rs from frost-hubert-rust.
|
|
399
|
+
*
|
|
400
|
+
* @module
|
|
401
|
+
*/
|
|
402
|
+
/**
|
|
403
|
+
* Parse an ARID from a UR string.
|
|
404
|
+
*
|
|
405
|
+
* Port of `parse_arid_ur()` from cmd/dkg/common.rs.
|
|
406
|
+
*/
|
|
407
|
+
function parseAridUr(urString) {
|
|
408
|
+
const ur = UR.fromURString(urString.trim());
|
|
409
|
+
if (ur.urTypeStr() !== "arid") throw new Error(`Expected ur:arid, found ur:${ur.urTypeStr()}`);
|
|
410
|
+
const { ARID: ARIDClass } = __require("@bcts/components");
|
|
411
|
+
return ARIDClass.fromCbor(ur.cbor());
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Parse an envelope from a UR string.
|
|
415
|
+
*
|
|
416
|
+
* Port of `parse_envelope_ur()` from cmd/dkg/common.rs.
|
|
417
|
+
*/
|
|
418
|
+
function parseEnvelopeUr(urString) {
|
|
419
|
+
const ur = UR.fromURString(urString.trim());
|
|
420
|
+
if (ur.urTypeStr() !== "envelope") throw new Error(`Expected ur:envelope, found ur:${ur.urTypeStr()}`);
|
|
421
|
+
const { Envelope: EnvelopeClass } = __require("@bcts/envelope");
|
|
422
|
+
return EnvelopeClass.fromCbor(ur.cbor());
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Resolve the sender XID document from the registry.
|
|
426
|
+
*
|
|
427
|
+
* Port of `resolve_sender()` from cmd/dkg/common.rs.
|
|
428
|
+
*/
|
|
429
|
+
function resolveSender(registry) {
|
|
430
|
+
const owner = registry.owner();
|
|
431
|
+
if (!owner) throw new Error("No owner set in registry. Run 'registry owner set' first.");
|
|
432
|
+
return owner.xidDocument();
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Resolve participant identifiers (XID URs or pet names) to records.
|
|
436
|
+
*
|
|
437
|
+
* Port of `resolve_participants()` from cmd/dkg/common.rs lines 29-74.
|
|
438
|
+
*/
|
|
439
|
+
function resolveParticipants(registry, inputs) {
|
|
440
|
+
const seenArgs = /* @__PURE__ */ new Set();
|
|
441
|
+
const seenXids = /* @__PURE__ */ new Set();
|
|
442
|
+
const resolved = [];
|
|
443
|
+
for (const raw of inputs) {
|
|
444
|
+
const trimmed = raw.trim();
|
|
445
|
+
if (trimmed === "") throw new Error("Participant identifier cannot be empty");
|
|
446
|
+
if (seenArgs.has(trimmed)) throw new Error(`Duplicate participant argument: ${trimmed}`);
|
|
447
|
+
seenArgs.add(trimmed);
|
|
448
|
+
let xid;
|
|
449
|
+
let record;
|
|
450
|
+
try {
|
|
451
|
+
const { XID: XIDClass } = __require("@bcts/components");
|
|
452
|
+
xid = XIDClass.fromURString(trimmed);
|
|
453
|
+
const foundRecord = registry.participant(xid);
|
|
454
|
+
if (!foundRecord) throw new Error(`Participant with XID ${xid.urString()} not found in registry`);
|
|
455
|
+
record = foundRecord;
|
|
456
|
+
} catch {
|
|
457
|
+
const result = registry.participantByPetName(trimmed);
|
|
458
|
+
if (!result) throw new Error(`Participant with pet name '${trimmed}' not found`);
|
|
459
|
+
[xid, record] = result;
|
|
460
|
+
}
|
|
461
|
+
const xidUr = xid.urString();
|
|
462
|
+
if (seenXids.has(xidUr)) throw new Error(`Duplicate participant specified; multiple inputs resolve to ${xidUr}`);
|
|
463
|
+
seenXids.add(xidUr);
|
|
464
|
+
resolved.push([xid, record]);
|
|
465
|
+
}
|
|
466
|
+
return resolved;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Get display name for sender from registry.
|
|
470
|
+
*
|
|
471
|
+
* Port of `resolve_sender_name()` from cmd/dkg/common.rs lines 96-116.
|
|
472
|
+
*/
|
|
473
|
+
function resolveSenderName$1(registry, sender) {
|
|
474
|
+
const owner = registry.owner();
|
|
475
|
+
const senderXid = sender.xid();
|
|
476
|
+
if (owner?.xidDocument().xid().urString() === senderXid.urString()) return formatNameWithOwnerMarker(owner.petName() ?? senderXid.urString(), true);
|
|
477
|
+
const record = registry.participant(senderXid);
|
|
478
|
+
if (record) return formatNameWithOwnerMarker(record.petName() ?? record.xid().urString(), false);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Build GroupParticipant[] from XIDDocuments using registry lookups.
|
|
482
|
+
*
|
|
483
|
+
* Port of `build_group_participants()` from cmd/dkg/common.rs lines 122-131.
|
|
484
|
+
*/
|
|
485
|
+
function buildGroupParticipants(registry, owner, participants) {
|
|
486
|
+
return participants.map((doc) => groupParticipantFromRegistry(registry, owner, doc));
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Create a GroupParticipant from a XIDDocument, validating against the registry.
|
|
490
|
+
*
|
|
491
|
+
* Port of `group_participant_from_registry()` from cmd/dkg/common.rs lines 133-149.
|
|
492
|
+
*/
|
|
493
|
+
function groupParticipantFromRegistry(registry, owner, document) {
|
|
494
|
+
const xid = document.xid();
|
|
495
|
+
if (xid.urString() === owner.xid().urString()) return new GroupParticipant(xid);
|
|
496
|
+
if (!registry.participant(xid)) throw new Error(`Invite participant not found in registry: ${xid.urString()}`);
|
|
497
|
+
return new GroupParticipant(xid);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Format a participant name with owner marker if applicable.
|
|
501
|
+
*
|
|
502
|
+
* Port of `format_name_with_owner_marker()` from cmd/dkg/common.rs lines 155-157.
|
|
503
|
+
*/
|
|
504
|
+
function formatNameWithOwnerMarker(name, isOwner) {
|
|
505
|
+
return isOwner ? `* ${name}` : name;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Get display names for participants, sorted by XID, with owner marked.
|
|
509
|
+
*
|
|
510
|
+
* Port of `participant_names_from_registry()` from cmd/dkg/common.rs lines 159-191.
|
|
511
|
+
*/
|
|
512
|
+
function participantNamesFromRegistry(registry, participants, ownerXid, ownerPetName) {
|
|
513
|
+
return [...participants].sort((a, b) => a.xid().urString().localeCompare(b.xid().urString())).map((document) => {
|
|
514
|
+
const xid = document.xid();
|
|
515
|
+
const isOwner = xid.urString() === ownerXid.urString();
|
|
516
|
+
let name;
|
|
517
|
+
if (isOwner) name = ownerPetName ?? xid.urString();
|
|
518
|
+
else {
|
|
519
|
+
const record = registry.participant(xid);
|
|
520
|
+
if (!record) throw new Error(`Invite participant not found in registry: ${xid.urString()}`);
|
|
521
|
+
name = record.petName() ?? xid.urString();
|
|
522
|
+
}
|
|
523
|
+
return formatNameWithOwnerMarker(name, isOwner);
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Get the DKG state directory for a given registry path and group ID.
|
|
528
|
+
*
|
|
529
|
+
* Port of `dkg_state_dir()` from cmd/dkg/common.rs.
|
|
530
|
+
*/
|
|
531
|
+
function dkgStateDir(registryPath, groupIdHex) {
|
|
532
|
+
const base = path.dirname(registryPath);
|
|
533
|
+
return path.join(base, "group-state", groupIdHex, "dkg");
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Convert a verifying key bytes to a SigningPublicKey.
|
|
537
|
+
*
|
|
538
|
+
* Port of `signing_key_from_verifying()` from cmd/dkg/common.rs.
|
|
539
|
+
*/
|
|
540
|
+
function signingKeyFromVerifying(verifyingKeyBytes) {
|
|
541
|
+
const { SigningPublicKey: SigningPublicKeyClass } = __require("@bcts/components");
|
|
542
|
+
return SigningPublicKeyClass.fromBytes(verifyingKeyBytes);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/cmd/dkg/coordinator/invite.ts
|
|
547
|
+
/**
|
|
548
|
+
* DKG coordinator invite command.
|
|
549
|
+
*
|
|
550
|
+
* Port of cmd/dkg/coordinator/invite.rs from frost-hubert-rust.
|
|
551
|
+
*
|
|
552
|
+
* @module
|
|
553
|
+
*/
|
|
554
|
+
/**
|
|
555
|
+
* Build the DKG invite with validation.
|
|
556
|
+
*
|
|
557
|
+
* Port of `build_invite()` from cmd/dkg/coordinator/invite.rs lines 128-181.
|
|
558
|
+
*/
|
|
559
|
+
function buildInvite(registry, minSignersArg, charter, participantNames, validDays) {
|
|
560
|
+
const resolved = resolveParticipants(registry, participantNames);
|
|
561
|
+
const participantDocs = resolved.map(([, record]) => record.xidDocumentUr());
|
|
562
|
+
const participantXids = resolved.map(([xid]) => xid);
|
|
563
|
+
const collectFromArids = participantDocs.map(() => ARID.new());
|
|
564
|
+
const pendingRequests = new PendingRequests();
|
|
565
|
+
for (let i = 0; i < participantXids.length; i++) pendingRequests.addCollectOnly(participantXids[i], collectFromArids[i]);
|
|
566
|
+
const participantCount = participantDocs.length;
|
|
567
|
+
if (participantCount < 2) throw new Error("At least two participants are required for a DKG invite");
|
|
568
|
+
const minSigners = minSignersArg ?? participantCount;
|
|
569
|
+
if (minSigners < 2) throw new Error("--min-signers must be at least 2");
|
|
570
|
+
if (minSigners > participantCount) throw new Error("--min-signers cannot exceed participant count");
|
|
571
|
+
const sender = resolveSender(registry);
|
|
572
|
+
const now = /* @__PURE__ */ new Date();
|
|
573
|
+
const validUntil = new Date(Date.now() + validDays * 24 * 60 * 60 * 1e3);
|
|
574
|
+
return {
|
|
575
|
+
invite: DkgInvite.create(ARID.new(), sender, ARID.new(), now, validUntil, minSigners, charter, participantDocs, collectFromArids),
|
|
576
|
+
participantXids,
|
|
577
|
+
pendingRequests
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Execute the DKG invite command.
|
|
582
|
+
*
|
|
583
|
+
* Port of `invite()` from cmd/dkg/coordinator/invite.rs.
|
|
584
|
+
*/
|
|
585
|
+
async function invite$1(client, options, cwd) {
|
|
586
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
587
|
+
const registry = Registry.load(registryPath);
|
|
588
|
+
const { invite: dkgInvite, participantXids, pendingRequests } = buildInvite(registry, options.minSigners, options.charter, options.participantNames, options.validDays);
|
|
589
|
+
const groupId = dkgInvite.groupId();
|
|
590
|
+
const requestId = dkgInvite.requestId();
|
|
591
|
+
const envelope = dkgInvite.toEnvelope();
|
|
592
|
+
const startArid = ARID.new();
|
|
593
|
+
await putWithIndicator(client, startArid, envelope, "Sending DKG invite", options.verbose ?? false);
|
|
594
|
+
const owner = registry.owner();
|
|
595
|
+
if (!owner) throw new Error("Registry owner is required to issue invites");
|
|
596
|
+
const coordinator = new GroupParticipant(owner.xid());
|
|
597
|
+
const groupParticipants = participantXids.map((xid) => new GroupParticipant(xid));
|
|
598
|
+
const groupRecord = new GroupRecord(options.charter, dkgInvite.minSigners(), coordinator, groupParticipants);
|
|
599
|
+
groupRecord.setPendingRequests(pendingRequests);
|
|
600
|
+
registry.recordGroup(groupId, groupRecord);
|
|
601
|
+
registry.save(registryPath);
|
|
602
|
+
const stateDir = dkgStateDir(registryPath, groupId.hex());
|
|
603
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
604
|
+
const inviteState = {
|
|
605
|
+
group: groupId.urString(),
|
|
606
|
+
request_id: requestId.urString(),
|
|
607
|
+
start_arid: startArid.urString(),
|
|
608
|
+
valid_until: dkgInvite.validUntil().toISOString(),
|
|
609
|
+
participants: dkgInvite.participants().map((p) => ({
|
|
610
|
+
xid: p.xid().urString(),
|
|
611
|
+
response_arid: p.responseArid().urString()
|
|
612
|
+
}))
|
|
613
|
+
};
|
|
614
|
+
fs.writeFileSync(path.join(stateDir, "invite.json"), JSON.stringify(inviteState, null, 2));
|
|
615
|
+
if (options.verbose === true) {
|
|
616
|
+
console.log(`Group ID: ${groupId.urString()}`);
|
|
617
|
+
console.log(`Start ARID: ${startArid.urString()}`);
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
groupId,
|
|
621
|
+
requestId,
|
|
622
|
+
envelopeUr: envelope.urString()
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
//#endregion
|
|
627
|
+
//#region src/cmd/dkg/coordinator/round1.ts
|
|
628
|
+
/**
|
|
629
|
+
* DKG coordinator round 1 command.
|
|
630
|
+
*
|
|
631
|
+
* Port of cmd/dkg/coordinator/round1.rs from frost-hubert-rust.
|
|
632
|
+
*
|
|
633
|
+
* @module
|
|
634
|
+
*/
|
|
635
|
+
/**
|
|
636
|
+
* Validate that the owner is the coordinator for this group.
|
|
637
|
+
*
|
|
638
|
+
* Port of `validate_coordinator()` from cmd/dkg/coordinator/round1.rs lines 201-214.
|
|
639
|
+
*/
|
|
640
|
+
function validateCoordinator$3(groupRecord, owner) {
|
|
641
|
+
if (groupRecord.coordinator().xid().urString() !== owner.xid().urString()) throw new Error(`Only the coordinator can collect and send Round 2 requests. Coordinator: ${groupRecord.coordinator().xid().urString()}, Owner: ${owner.xid().urString()}`);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Fetch a single round 1 response from storage.
|
|
645
|
+
*
|
|
646
|
+
* Port of `fetch_and_validate_response()` from cmd/dkg/coordinator/round1.rs lines 512-566.
|
|
647
|
+
*/
|
|
648
|
+
async function fetchRound1Response(client, responseArid, timeout, coordinator, expectedGroupId, participantName) {
|
|
649
|
+
const envelope = await getWithIndicator(client, responseArid, participantName, timeout, isVerbose());
|
|
650
|
+
if (envelope === void 0) throw new Error("Response not found in Hubert storage");
|
|
651
|
+
const coordinatorPrivateKeys = coordinator.inceptionPrivateKeys();
|
|
652
|
+
if (coordinatorPrivateKeys === void 0) throw new Error("Coordinator XID document has no inception private keys");
|
|
653
|
+
const now = CborDate.now().datetime();
|
|
654
|
+
const sealedResponse = SealedResponse.tryFromEncryptedEnvelope(envelope, void 0, now, coordinatorPrivateKeys);
|
|
655
|
+
if (sealedResponse.isErr()) {
|
|
656
|
+
const reasonEnvelope = sealedResponse.error().objectForPredicate("reason");
|
|
657
|
+
const reason = reasonEnvelope !== void 0 ? reasonEnvelope.extractSubject((cbor) => cbor.toText()) : "unknown reason";
|
|
658
|
+
throw new Error(`Participant rejected invite: ${reason}`);
|
|
659
|
+
}
|
|
660
|
+
const result = sealedResponse.result();
|
|
661
|
+
validateRound1Response(result, expectedGroupId);
|
|
662
|
+
const nextResponseArid = result.tryObjectForPredicate("response_arid", (cbor) => {
|
|
663
|
+
return ARID.fromTaggedCbor(cbor);
|
|
664
|
+
});
|
|
665
|
+
return [extractRound1Package(result), nextResponseArid];
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Validate a round 1 response envelope.
|
|
669
|
+
*
|
|
670
|
+
* Port of `validate_round1_response()` from cmd/dkg/coordinator/round1.rs lines 568-586.
|
|
671
|
+
*/
|
|
672
|
+
function validateRound1Response(result, expectedGroupId) {
|
|
673
|
+
result.checkSubjectUnit();
|
|
674
|
+
result.checkType("dkgRound1Response");
|
|
675
|
+
const groupId = result.tryObjectForPredicate("group", (cbor) => {
|
|
676
|
+
return ARID.fromTaggedCbor(cbor);
|
|
677
|
+
});
|
|
678
|
+
if (groupId.urString() !== expectedGroupId.urString()) throw new Error(`Response group ID ${groupId.urString()} does not match expected ${expectedGroupId.urString()}`);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Extract a round 1 package from a response envelope.
|
|
682
|
+
*
|
|
683
|
+
* Port of `extract_round1_package()` from cmd/dkg/coordinator/round1.rs lines 588-598.
|
|
684
|
+
*/
|
|
685
|
+
function extractRound1Package(result) {
|
|
686
|
+
const round1Envelope = result.objectForPredicate("round1_package");
|
|
687
|
+
if (round1Envelope === void 0) throw new Error("round1_package missing from response");
|
|
688
|
+
const round1Json = round1Envelope.extractSubject((cbor) => {
|
|
689
|
+
return JSON$1.fromTaggedCbor(cbor);
|
|
690
|
+
});
|
|
691
|
+
const jsonStr = new TextDecoder().decode(round1Json.asBytes());
|
|
692
|
+
const jsonObj = globalThis.JSON.parse(jsonStr);
|
|
693
|
+
return serde.round1PackageFromJson(jsonObj);
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Validate and extract data from a round 1 response (for parallel fetch).
|
|
697
|
+
*
|
|
698
|
+
* Port of `validate_and_extract_round1_response()` from cmd/dkg/coordinator/round1.rs lines 674-707.
|
|
699
|
+
*/
|
|
700
|
+
function validateAndExtractRound1Response(envelope, coordinatorKeys, expectedGroupId) {
|
|
701
|
+
const now = CborDate.now().datetime();
|
|
702
|
+
const sealedResponse = SealedResponse.tryFromEncryptedEnvelope(envelope, void 0, now, coordinatorKeys);
|
|
703
|
+
if (sealedResponse.isErr()) {
|
|
704
|
+
const reasonEnvelope = sealedResponse.error().objectForPredicate("reason");
|
|
705
|
+
return { rejected: `Participant rejected invite: ${reasonEnvelope !== void 0 ? reasonEnvelope.extractSubject((cbor) => cbor.toText()) : "unknown reason"}` };
|
|
706
|
+
}
|
|
707
|
+
const result = sealedResponse.result();
|
|
708
|
+
validateRound1Response(result, expectedGroupId);
|
|
709
|
+
const nextResponseArid = result.tryObjectForPredicate("response_arid", (cbor) => {
|
|
710
|
+
return ARID.fromTaggedCbor(cbor);
|
|
711
|
+
});
|
|
712
|
+
return {
|
|
713
|
+
package: extractRound1Package(result),
|
|
714
|
+
nextResponseArid
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Collect Round 1 responses in parallel with progress display.
|
|
719
|
+
*
|
|
720
|
+
* Port of `collect_round1_responses_parallel()` from cmd/dkg/coordinator/round1.rs lines 636-671.
|
|
721
|
+
*/
|
|
722
|
+
async function collectRound1Parallel(client, registry, pendingRequests, coordinator, expectedGroupId, timeout) {
|
|
723
|
+
const requests = [];
|
|
724
|
+
for (const [xid, arid] of pendingRequests.iterCollect()) {
|
|
725
|
+
const name = registry.participant(xid)?.petName() ?? xid.urString();
|
|
726
|
+
requests.push([
|
|
727
|
+
xid,
|
|
728
|
+
arid,
|
|
729
|
+
name
|
|
730
|
+
]);
|
|
731
|
+
}
|
|
732
|
+
const coordinatorKeys = coordinator.inceptionPrivateKeys();
|
|
733
|
+
if (coordinatorKeys === void 0) throw new Error("Missing coordinator private keys");
|
|
734
|
+
const config = { timeoutSeconds: timeout };
|
|
735
|
+
const groupId = expectedGroupId;
|
|
736
|
+
return parallelFetch(client, requests, (envelope, _xid) => {
|
|
737
|
+
return validateAndExtractRound1Response(envelope, coordinatorKeys, groupId);
|
|
738
|
+
}, config);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Build a Round 2 request for a single participant.
|
|
742
|
+
*
|
|
743
|
+
* Port of `build_round2_request_for_participant()` from cmd/dkg/coordinator/round1.rs lines 604-623.
|
|
744
|
+
*/
|
|
745
|
+
function buildRound2RequestForParticipant(sender, groupId, round1Packages, responseArid) {
|
|
746
|
+
let request = SealedRequest.new("dkgRound2", ARID.new(), sender).withParameter("group", groupId).withParameter("responseArid", responseArid);
|
|
747
|
+
for (const [xid, pkg] of round1Packages) {
|
|
748
|
+
const packageJson = serde.round1PackageToJson(pkg);
|
|
749
|
+
const jsonStr = globalThis.JSON.stringify(packageJson);
|
|
750
|
+
const jsonBytes = new TextEncoder().encode(jsonStr);
|
|
751
|
+
const jsonWrapper = JSON$1.fromData(jsonBytes);
|
|
752
|
+
const packageEnvelope = Envelope.new(jsonWrapper).addAssertion("participant", xid);
|
|
753
|
+
request = request.withParameter("round1Package", packageEnvelope);
|
|
754
|
+
}
|
|
755
|
+
return request;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Dispatch Round 2 requests in parallel.
|
|
759
|
+
*
|
|
760
|
+
* Port of `dispatch_round2_requests_parallel()` from cmd/dkg/coordinator/round1.rs lines 729-844.
|
|
761
|
+
*/
|
|
762
|
+
async function dispatchRound2RequestsParallel(client, registry, registryPath, coordinator, groupId, successes, preview) {
|
|
763
|
+
const signerPrivateKeys = coordinator.inceptionPrivateKeys();
|
|
764
|
+
if (signerPrivateKeys === void 0) throw new Error("Coordinator XID document has no signing keys");
|
|
765
|
+
const validUntil = new Date(Date.now() + 3600 * 1e3);
|
|
766
|
+
const round1Packages = successes.map(([xid, data]) => [xid, data.package]);
|
|
767
|
+
const messages = [];
|
|
768
|
+
const collectArids = [];
|
|
769
|
+
let previewOutput;
|
|
770
|
+
for (const [xid, data] of successes) {
|
|
771
|
+
const participant = registry.participant(xid);
|
|
772
|
+
if (participant === void 0) throw new Error(`Participant ${xid.urString()} not found in registry`);
|
|
773
|
+
const recipientDoc = participant.xidDocument();
|
|
774
|
+
const participantName = participant.petName() ?? xid.urString();
|
|
775
|
+
const collectFromArid = ARID.new();
|
|
776
|
+
collectArids.push([xid, collectFromArid]);
|
|
777
|
+
const request = buildRound2RequestForParticipant(coordinator, groupId, round1Packages, collectFromArid);
|
|
778
|
+
if (preview && previewOutput === void 0) previewOutput = [participantName, request.toEnvelope(validUntil, signerPrivateKeys, void 0).urString()];
|
|
779
|
+
const sealedEnvelope = request.toEnvelope(validUntil, signerPrivateKeys, recipientDoc);
|
|
780
|
+
messages.push([
|
|
781
|
+
xid,
|
|
782
|
+
data.nextResponseArid,
|
|
783
|
+
sealedEnvelope,
|
|
784
|
+
participantName
|
|
785
|
+
]);
|
|
786
|
+
}
|
|
787
|
+
console.error();
|
|
788
|
+
const sendResults = await parallelSend(client, messages, isVerbose());
|
|
789
|
+
const failures = [];
|
|
790
|
+
for (const [xid, err] of sendResults) if (err !== null) failures.push([xid, err.message]);
|
|
791
|
+
if (failures.length > 0) {
|
|
792
|
+
for (const [xid, error] of failures) console.error(`Failed to send to ${xid.urString()}: ${error}`);
|
|
793
|
+
throw new Error(`Failed to send Round 2 requests to ${failures.length} participants`);
|
|
794
|
+
}
|
|
795
|
+
const newPendingRequests = new PendingRequests();
|
|
796
|
+
for (const [xid, collectFromArid] of collectArids) newPendingRequests.addCollectOnly(xid, collectFromArid);
|
|
797
|
+
const groupRecord = registry.groupMut(groupId);
|
|
798
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
799
|
+
groupRecord.setPendingRequests(newPendingRequests);
|
|
800
|
+
registry.save(registryPath);
|
|
801
|
+
return previewOutput;
|
|
802
|
+
}
|
|
803
|
+
/**
|
|
804
|
+
* Update pending requests from parallel collection results.
|
|
805
|
+
*
|
|
806
|
+
* Port of `update_pending_for_round2_from_collection()` from cmd/dkg/coordinator/round1.rs lines 710-726.
|
|
807
|
+
*/
|
|
808
|
+
function updatePendingForRound2FromCollection(registry, registryPath, groupId, successes) {
|
|
809
|
+
const newPending = new PendingRequests();
|
|
810
|
+
for (const [xid, data] of successes) newPending.addSendOnly(xid, data.nextResponseArid);
|
|
811
|
+
const groupRecord = registry.groupMut(groupId);
|
|
812
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
813
|
+
groupRecord.setPendingRequests(newPending);
|
|
814
|
+
registry.save(registryPath);
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Persist collected round 1 packages to disk.
|
|
818
|
+
*
|
|
819
|
+
* Port of `persist_round1_packages()` from cmd/dkg/coordinator/round1.rs lines 301-340.
|
|
820
|
+
*/
|
|
821
|
+
function persistRound1Packages(registryPath, groupId, packages) {
|
|
822
|
+
const packagesDir = groupStateDir(registryPath, groupId.hex());
|
|
823
|
+
fs.mkdirSync(packagesDir, { recursive: true });
|
|
824
|
+
const round1PackagesPath = path.join(packagesDir, "collected_round1.json");
|
|
825
|
+
const packagesJson = {};
|
|
826
|
+
for (const [xid, pkg] of packages) packagesJson[xid.urString()] = serde.round1PackageToJson(pkg);
|
|
827
|
+
fs.writeFileSync(round1PackagesPath, globalThis.JSON.stringify(packagesJson, null, 2));
|
|
828
|
+
const cwd = process.cwd();
|
|
829
|
+
if (round1PackagesPath.startsWith(cwd)) return round1PackagesPath.slice(cwd.length + 1);
|
|
830
|
+
return round1PackagesPath;
|
|
831
|
+
}
|
|
832
|
+
/**
|
|
833
|
+
* Fetch all round 1 packages sequentially.
|
|
834
|
+
*
|
|
835
|
+
* Port of `fetch_all_round1_packages()` from cmd/dkg/coordinator/round1.rs lines 243-299.
|
|
836
|
+
*/
|
|
837
|
+
async function fetchAllRound1Packages(ctx, pendingRequests, timeout) {
|
|
838
|
+
const round1Packages = [];
|
|
839
|
+
const nextResponseArids = [];
|
|
840
|
+
const errors = [];
|
|
841
|
+
for (const [participantXid, collectFromArid] of pendingRequests.iterCollect()) {
|
|
842
|
+
const participantName = ctx.registry.participant(participantXid)?.petName() ?? participantXid.urString();
|
|
843
|
+
try {
|
|
844
|
+
const [pkg, nextArid] = await fetchRound1Response(ctx.client, collectFromArid, timeout, ctx.ownerDoc, ctx.groupId, participantName);
|
|
845
|
+
round1Packages.push([participantXid, pkg]);
|
|
846
|
+
nextResponseArids.push([participantXid, nextArid]);
|
|
847
|
+
} catch (e) {
|
|
848
|
+
errors.push([participantXid, e instanceof Error ? e.message : String(e)]);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (errors.length > 0) {
|
|
852
|
+
if (isVerbose()) {
|
|
853
|
+
console.error();
|
|
854
|
+
console.error(`Failed to collect from ${errors.length} participants:`);
|
|
855
|
+
}
|
|
856
|
+
for (const [xid, error] of errors) console.error(` ${xid.urString()}: ${error}`);
|
|
857
|
+
throw new Error(`Round 1 collection incomplete: ${errors.length} of ${pendingRequests.len()} responses failed`);
|
|
858
|
+
}
|
|
859
|
+
return [round1Packages, nextResponseArids];
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Collect round 1 responses sequentially.
|
|
863
|
+
*
|
|
864
|
+
* Port of `collect_round1_responses()` from cmd/dkg/coordinator/round1.rs lines 220-241.
|
|
865
|
+
*/
|
|
866
|
+
async function collectRound1Responses(ctx, pendingRequests, timeout) {
|
|
867
|
+
if (isVerbose()) console.error(`Collecting Round 1 responses from ${pendingRequests.len()} participants...`);
|
|
868
|
+
const [packages, nextResponseArids] = await fetchAllRound1Packages(ctx, pendingRequests, timeout);
|
|
869
|
+
const displayPath = persistRound1Packages(ctx.registryPath, ctx.groupId, packages);
|
|
870
|
+
updatePendingForRound2(ctx, nextResponseArids);
|
|
871
|
+
return {
|
|
872
|
+
packages,
|
|
873
|
+
nextResponseArids,
|
|
874
|
+
displayPath
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Update pending requests for round 2 (sequential path).
|
|
879
|
+
*
|
|
880
|
+
* Port of `update_pending_for_round2()` from cmd/dkg/coordinator/round1.rs lines 342-357.
|
|
881
|
+
*/
|
|
882
|
+
function updatePendingForRound2(ctx, nextResponseArids) {
|
|
883
|
+
const newPending = new PendingRequests();
|
|
884
|
+
for (const [xid, sendToArid] of nextResponseArids) newPending.addSendOnly(xid, sendToArid);
|
|
885
|
+
const groupRecord = ctx.registry.groupMut(ctx.groupId);
|
|
886
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
887
|
+
groupRecord.setPendingRequests(newPending);
|
|
888
|
+
ctx.registry.save(ctx.registryPath);
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Build participant info for round 2 dispatch.
|
|
892
|
+
*
|
|
893
|
+
* Port of `build_round2_participant_info()` from cmd/dkg/coordinator/round1.rs lines 438-458.
|
|
894
|
+
*/
|
|
895
|
+
function buildRound2ParticipantInfo(registry, nextResponseArids) {
|
|
896
|
+
return nextResponseArids.map(([xid, sendToArid]) => {
|
|
897
|
+
const participant = registry.participant(xid);
|
|
898
|
+
if (participant === void 0) throw new Error(`Participant ${xid.urString()} not found in registry`);
|
|
899
|
+
return [
|
|
900
|
+
xid,
|
|
901
|
+
participant.xidDocument(),
|
|
902
|
+
sendToArid,
|
|
903
|
+
ARID.new()
|
|
904
|
+
];
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Update pending for round 2 collection (sequential path).
|
|
909
|
+
*
|
|
910
|
+
* Port of `update_pending_for_round2_collection()` from cmd/dkg/coordinator/round1.rs lines 460-475.
|
|
911
|
+
*/
|
|
912
|
+
function updatePendingForRound2Collection(ctx, participantInfo) {
|
|
913
|
+
const newPendingRequests = new PendingRequests();
|
|
914
|
+
for (const [xid, , , collectFromArid] of participantInfo) newPendingRequests.addCollectOnly(xid, collectFromArid);
|
|
915
|
+
const groupRecord = ctx.registry.groupMut(ctx.groupId);
|
|
916
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
917
|
+
groupRecord.setPendingRequests(newPendingRequests);
|
|
918
|
+
ctx.registry.save(ctx.registryPath);
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Dispatch Round 2 requests sequentially.
|
|
922
|
+
*
|
|
923
|
+
* Port of `dispatch_round2_requests()` from cmd/dkg/coordinator/round1.rs lines 363-436.
|
|
924
|
+
*/
|
|
925
|
+
async function dispatchRound2Requests(ctx, collection, preview) {
|
|
926
|
+
const signerPrivateKeys = ctx.ownerDoc.inceptionPrivateKeys();
|
|
927
|
+
if (signerPrivateKeys === void 0) throw new Error("Coordinator XID document has no signing keys");
|
|
928
|
+
const validUntil = new Date(Date.now() + 3600 * 1e3);
|
|
929
|
+
const participantInfo = buildRound2ParticipantInfo(ctx.registry, collection.nextResponseArids);
|
|
930
|
+
if (isVerbose()) console.error(`Sending Round 2 requests to ${participantInfo.length} participants...`);
|
|
931
|
+
else console.error();
|
|
932
|
+
let previewOutput;
|
|
933
|
+
for (const [xid, recipientDoc, sendToArid, collectFromArid] of participantInfo) {
|
|
934
|
+
const participantName = ctx.registry.participant(xid)?.petName() ?? xid.urString();
|
|
935
|
+
const request = buildRound2RequestForParticipant(ctx.ownerDoc, ctx.groupId, collection.packages, collectFromArid);
|
|
936
|
+
if (preview && previewOutput === void 0) previewOutput = [participantName, request.toEnvelope(validUntil, signerPrivateKeys, void 0).urString()];
|
|
937
|
+
const sealedEnvelope = request.toEnvelope(validUntil, signerPrivateKeys, recipientDoc);
|
|
938
|
+
await putWithIndicator(ctx.client, sendToArid, sealedEnvelope, participantName, isVerbose());
|
|
939
|
+
}
|
|
940
|
+
updatePendingForRound2Collection(ctx, participantInfo);
|
|
941
|
+
return previewOutput;
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Print summary for sequential collection.
|
|
945
|
+
*
|
|
946
|
+
* Port of `print_summary()` from cmd/dkg/coordinator/round1.rs lines 481-506.
|
|
947
|
+
*/
|
|
948
|
+
function printSummary(collection, preview) {
|
|
949
|
+
if (preview !== void 0) {
|
|
950
|
+
const [participantName, ur] = preview;
|
|
951
|
+
if (isVerbose()) {
|
|
952
|
+
console.error(`# Round 2 preview for ${participantName}`);
|
|
953
|
+
console.error();
|
|
954
|
+
}
|
|
955
|
+
console.error(`Collected ${collection.packages.length} Round 1 packages to ${collection.displayPath} and sent ${collection.nextResponseArids.length} Round 2 requests.`);
|
|
956
|
+
console.log(ur);
|
|
957
|
+
} else if (isVerbose()) {
|
|
958
|
+
console.error();
|
|
959
|
+
console.error(`Collected ${collection.packages.length} Round 1 packages to ${collection.displayPath} and sent ${collection.nextResponseArids.length} Round 2 requests.`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Print summary for parallel collection.
|
|
964
|
+
*
|
|
965
|
+
* Port of `print_summary_parallel()` from cmd/dkg/coordinator/round1.rs lines 847-901.
|
|
966
|
+
*/
|
|
967
|
+
function printSummaryParallel$1(collection, displayPath, preview) {
|
|
968
|
+
if (collection.rejections.length > 0) {
|
|
969
|
+
console.error();
|
|
970
|
+
console.error("Rejections:");
|
|
971
|
+
for (const [xid, reason] of collection.rejections) console.error(` ${xid.urString()}: ${reason}`);
|
|
972
|
+
}
|
|
973
|
+
if (collection.errors.length > 0) {
|
|
974
|
+
console.error();
|
|
975
|
+
console.error("Errors:");
|
|
976
|
+
for (const [xid, error] of collection.errors) console.error(` ${xid.urString()}: ${error}`);
|
|
977
|
+
}
|
|
978
|
+
if (collection.timeouts.length > 0) {
|
|
979
|
+
console.error();
|
|
980
|
+
console.error("Timeouts:");
|
|
981
|
+
for (const xid of collection.timeouts) console.error(` ${xid.urString()}`);
|
|
982
|
+
}
|
|
983
|
+
if (!collection.allSucceeded()) {
|
|
984
|
+
console.error();
|
|
985
|
+
bailWithCollectionSummary(collection);
|
|
986
|
+
}
|
|
987
|
+
if (preview !== void 0) {
|
|
988
|
+
const [participantName, ur] = preview;
|
|
989
|
+
if (isVerbose()) {
|
|
990
|
+
console.error(`# Round 2 preview for ${participantName}`);
|
|
991
|
+
console.error();
|
|
992
|
+
}
|
|
993
|
+
console.error(`Collected ${collection.successes.length} Round 1 packages to ${displayPath} and sent ${collection.successes.length} Round 2 requests.`);
|
|
994
|
+
console.log(ur);
|
|
995
|
+
} else if (isVerbose()) {
|
|
996
|
+
console.error();
|
|
997
|
+
console.error(`Collected ${collection.successes.length} Round 1 packages to ${displayPath} and sent ${collection.successes.length} Round 2 requests.`);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Print collection summary and throw error.
|
|
1002
|
+
*
|
|
1003
|
+
* Port of `bail_with_collection_summary()` from cmd/dkg/coordinator/round1.rs lines 903-913.
|
|
1004
|
+
*/
|
|
1005
|
+
function bailWithCollectionSummary(collection) {
|
|
1006
|
+
const msg = `Round 1 collection incomplete: ${collection.successes.length} succeeded, ${collection.rejections.length} rejected, ${collection.errors.length} errors, ${collection.timeouts.length} timeouts`;
|
|
1007
|
+
console.error(msg);
|
|
1008
|
+
throw new Error(msg);
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Execute the DKG coordinator round 1 command.
|
|
1012
|
+
*
|
|
1013
|
+
* Collects commitment packages from participants.
|
|
1014
|
+
*
|
|
1015
|
+
* Port of `CommandArgs::exec()` from cmd/dkg/coordinator/round1.rs lines 59-173.
|
|
1016
|
+
*/
|
|
1017
|
+
async function round1$4(client, options, cwd) {
|
|
1018
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
1019
|
+
const registry = Registry.load(registryPath);
|
|
1020
|
+
const owner = registry.owner();
|
|
1021
|
+
if (owner === void 0) throw new Error("Registry owner is required");
|
|
1022
|
+
const ownerDoc = owner.xidDocument();
|
|
1023
|
+
const groupId = parseAridUr(options.groupId);
|
|
1024
|
+
const groupRecord = registry.group(groupId);
|
|
1025
|
+
if (groupRecord === void 0) throw new Error(`Group ${options.groupId} not found in registry`);
|
|
1026
|
+
validateCoordinator$3(groupRecord, owner);
|
|
1027
|
+
const pendingRequests = groupRecord.pendingRequests();
|
|
1028
|
+
if (pendingRequests.isEmpty()) throw new Error("No pending requests for this group. Round 1 may already be collected.");
|
|
1029
|
+
if (options.parallel === true) {
|
|
1030
|
+
const collection = await collectRound1Parallel(client, registry, pendingRequests, ownerDoc, groupId, options.timeoutSeconds);
|
|
1031
|
+
const displayPath = persistRound1Packages(registryPath, groupId, collection.successes.map(([xid, data]) => [xid, data.package]));
|
|
1032
|
+
updatePendingForRound2FromCollection(registry, registryPath, groupId, collection.successes);
|
|
1033
|
+
printSummaryParallel$1(collection, displayPath, await dispatchRound2RequestsParallel(client, registry, registryPath, ownerDoc, groupId, collection.successes, options.preview ?? false));
|
|
1034
|
+
return {
|
|
1035
|
+
accepted: collection.successes.length,
|
|
1036
|
+
rejected: collection.rejections.length,
|
|
1037
|
+
errors: collection.errors.length,
|
|
1038
|
+
timeouts: collection.timeouts.length
|
|
1039
|
+
};
|
|
1040
|
+
} else {
|
|
1041
|
+
const ctx = {
|
|
1042
|
+
client,
|
|
1043
|
+
registryPath,
|
|
1044
|
+
registry,
|
|
1045
|
+
ownerDoc,
|
|
1046
|
+
groupId
|
|
1047
|
+
};
|
|
1048
|
+
const collection = await collectRound1Responses(ctx, pendingRequests, options.timeoutSeconds);
|
|
1049
|
+
printSummary(collection, await dispatchRound2Requests(ctx, collection, options.preview ?? false));
|
|
1050
|
+
return {
|
|
1051
|
+
accepted: collection.packages.length,
|
|
1052
|
+
rejected: 0,
|
|
1053
|
+
errors: 0,
|
|
1054
|
+
timeouts: 0
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
//#endregion
|
|
1060
|
+
//#region src/cmd/dkg/coordinator/round2.ts
|
|
1061
|
+
/**
|
|
1062
|
+
* DKG coordinator round 2 command.
|
|
1063
|
+
*
|
|
1064
|
+
* Port of cmd/dkg/coordinator/round2.rs from frost-hubert-rust.
|
|
1065
|
+
*
|
|
1066
|
+
* @module
|
|
1067
|
+
*/
|
|
1068
|
+
/**
|
|
1069
|
+
* Validate that the owner is the coordinator of the group.
|
|
1070
|
+
*
|
|
1071
|
+
* Port of coordinator check from round2.rs lines 86-94.
|
|
1072
|
+
*/
|
|
1073
|
+
function validateCoordinator$2(groupRecord, ownerXid) {
|
|
1074
|
+
if (groupRecord.coordinator().xid().urString() !== ownerXid.urString()) throw new Error(`Only the coordinator can collect Round 2 responses and send finalize packages. Coordinator: ${groupRecord.coordinator().xid().urString()}, Owner: ${ownerXid.urString()}`);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Validate envelope and extract Round 2 data (for parallel fetch).
|
|
1078
|
+
*
|
|
1079
|
+
* Port of `validate_and_extract_round2_response()` from round2.rs lines 646-708.
|
|
1080
|
+
*/
|
|
1081
|
+
function validateAndExtractRound2Response(envelope, coordinatorKeys, expectedGroupId, expectedSender) {
|
|
1082
|
+
const now = /* @__PURE__ */ new Date();
|
|
1083
|
+
let sealed;
|
|
1084
|
+
try {
|
|
1085
|
+
sealed = SealedResponse.tryFromEncryptedEnvelope(envelope, void 0, now, coordinatorKeys);
|
|
1086
|
+
} catch (err) {
|
|
1087
|
+
return { rejected: `Failed to decrypt/parse response: ${err instanceof Error ? err.message : String(err)}` };
|
|
1088
|
+
}
|
|
1089
|
+
if (!sealed.isOk()) try {
|
|
1090
|
+
return { rejected: `Participant reported error: ${sealed.error().optionalObjectForPredicate("reason")?.extractString() ?? "unknown reason"}` };
|
|
1091
|
+
} catch {
|
|
1092
|
+
return { rejected: "Participant reported error: unknown reason" };
|
|
1093
|
+
}
|
|
1094
|
+
let result;
|
|
1095
|
+
try {
|
|
1096
|
+
result = sealed.result();
|
|
1097
|
+
} catch {
|
|
1098
|
+
return { rejected: "Response has no result envelope" };
|
|
1099
|
+
}
|
|
1100
|
+
try {
|
|
1101
|
+
result.checkSubjectUnit();
|
|
1102
|
+
result.checkType("dkgRound2Response");
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
return { rejected: `Invalid response type: ${err instanceof Error ? err.message : String(err)}` };
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
const groupId = result.objectForPredicate("group").extractSubject((cbor) => ARID.fromTaggedCbor(cbor));
|
|
1108
|
+
if (groupId.urString() !== expectedGroupId.urString()) return { rejected: `Response group ID ${groupId.urString()} does not match expected ${expectedGroupId.urString()}` };
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
return { rejected: `Failed to extract group: ${err instanceof Error ? err.message : String(err)}` };
|
|
1111
|
+
}
|
|
1112
|
+
try {
|
|
1113
|
+
const senderXid = result.objectForPredicate("participant").extractSubject((cbor) => XID.fromTaggedCbor(cbor));
|
|
1114
|
+
if (senderXid.urString() !== expectedSender.urString()) return { rejected: `Response participant ${senderXid.urString()} does not match expected ${expectedSender.urString()}` };
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
return { rejected: `Failed to extract participant: ${err instanceof Error ? err.message : String(err)}` };
|
|
1117
|
+
}
|
|
1118
|
+
let nextResponseArid;
|
|
1119
|
+
try {
|
|
1120
|
+
nextResponseArid = result.objectForPredicate("response_arid").extractSubject((cbor) => ARID.fromTaggedCbor(cbor));
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
return { rejected: `Failed to extract response_arid: ${err instanceof Error ? err.message : String(err)}` };
|
|
1123
|
+
}
|
|
1124
|
+
const packages = [];
|
|
1125
|
+
try {
|
|
1126
|
+
const pkgEnvelopes = result.objectsForPredicate("round2Package");
|
|
1127
|
+
for (const pkgEnv of pkgEnvelopes) {
|
|
1128
|
+
const recipient = pkgEnv.objectForPredicate("recipient").extractSubject((cbor) => XID.fromTaggedCbor(cbor));
|
|
1129
|
+
const pkgJsonStr = pkgEnv.extractString();
|
|
1130
|
+
const pkg = JSON.parse(pkgJsonStr);
|
|
1131
|
+
packages.push([recipient, pkg]);
|
|
1132
|
+
}
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
return { rejected: `Failed to extract round2 packages: ${err instanceof Error ? err.message : String(err)}` };
|
|
1135
|
+
}
|
|
1136
|
+
return {
|
|
1137
|
+
packages,
|
|
1138
|
+
nextResponseArid
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Fetch a Round 2 response sequentially.
|
|
1143
|
+
*
|
|
1144
|
+
* Port of `fetch_round2_response()` from round2.rs lines 364-442.
|
|
1145
|
+
*/
|
|
1146
|
+
async function fetchRound2Response(client, arid, timeout, coordinatorKeys, expectedGroup, expectedSender) {
|
|
1147
|
+
const envelope = await getWithIndicator(client, arid, expectedSender.urString(), timeout, isVerbose());
|
|
1148
|
+
if (envelope === null || envelope === void 0) throw new Error("Response not found in Hubert storage");
|
|
1149
|
+
const result = validateAndExtractRound2Response(envelope, coordinatorKeys, expectedGroup, expectedSender);
|
|
1150
|
+
if ("rejected" in result) throw new Error(result.rejected);
|
|
1151
|
+
return {
|
|
1152
|
+
packages: result.packages,
|
|
1153
|
+
nextResponseArid: result.nextResponseArid
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Collect Round 2 responses sequentially.
|
|
1158
|
+
*
|
|
1159
|
+
* Port of `collect_round2()` from round2.rs lines 216-357.
|
|
1160
|
+
*/
|
|
1161
|
+
async function collectRound2(client, registryPath, registry, coordinatorKeys, groupId, pendingRequests, timeout) {
|
|
1162
|
+
if (isVerbose()) console.error(`Collecting Round 2 responses from ${pendingRequests.len()} participants...`);
|
|
1163
|
+
const allPackages = /* @__PURE__ */ new Map();
|
|
1164
|
+
const nextResponseArids = [];
|
|
1165
|
+
const errors = [];
|
|
1166
|
+
for (const [participantXid, collectFromArid] of pendingRequests.iterCollect()) {
|
|
1167
|
+
const participantName = registry.participant(participantXid)?.petName() ?? participantXid.urString();
|
|
1168
|
+
if (isVerbose()) console.error(`${participantName}...`);
|
|
1169
|
+
try {
|
|
1170
|
+
const collected = await fetchRound2Response(client, collectFromArid, timeout, coordinatorKeys, groupId, participantXid);
|
|
1171
|
+
allPackages.set(participantXid.urString(), collected.packages);
|
|
1172
|
+
nextResponseArids.push([participantXid, collected.nextResponseArid]);
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
if (isVerbose()) console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1175
|
+
errors.push([participantXid, err instanceof Error ? err.message : String(err)]);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
if (errors.length > 0) {
|
|
1179
|
+
if (isVerbose()) {
|
|
1180
|
+
console.error();
|
|
1181
|
+
console.error(`Failed to collect from ${errors.length} participants:`);
|
|
1182
|
+
}
|
|
1183
|
+
for (const [xid, error] of errors) console.error(` ${xid.urString()}: ${error}`);
|
|
1184
|
+
throw new Error(`Round 2 collection incomplete: ${errors.length} of ${pendingRequests.len()} responses failed`);
|
|
1185
|
+
}
|
|
1186
|
+
const displayPath = persistRound2PackagesFromMap(registryPath, groupId, allPackages, nextResponseArids);
|
|
1187
|
+
const newPending = new PendingRequests();
|
|
1188
|
+
for (const [xid, sendToArid] of nextResponseArids) newPending.addSendOnly(xid, sendToArid);
|
|
1189
|
+
const groupRecord = registry.group(groupId);
|
|
1190
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
1191
|
+
groupRecord.setPendingRequests(newPending);
|
|
1192
|
+
registry.save(registryPath);
|
|
1193
|
+
return {
|
|
1194
|
+
packages: allPackages,
|
|
1195
|
+
nextResponseArids,
|
|
1196
|
+
displayPath
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Persist Round 2 packages from a Map (sequential collection).
|
|
1201
|
+
*/
|
|
1202
|
+
function persistRound2PackagesFromMap(registryPath, groupId, allPackages, nextResponseArids) {
|
|
1203
|
+
const stateDir = groupStateDir(registryPath, groupId.hex());
|
|
1204
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
1205
|
+
const collectedPath = path.join(stateDir, "collected_round2.json");
|
|
1206
|
+
const root = {};
|
|
1207
|
+
for (const [senderUrString, packages] of allPackages) {
|
|
1208
|
+
const senderMap = {};
|
|
1209
|
+
const responseArid = nextResponseArids.find(([xid]) => xid.urString() === senderUrString)?.[1];
|
|
1210
|
+
if (responseArid !== void 0) senderMap["response_arid"] = responseArid.urString();
|
|
1211
|
+
const packagesJson = {};
|
|
1212
|
+
for (const [recipient, pkg] of packages) packagesJson[recipient.urString()] = pkg;
|
|
1213
|
+
senderMap["packages"] = packagesJson;
|
|
1214
|
+
root[senderUrString] = senderMap;
|
|
1215
|
+
}
|
|
1216
|
+
fs.writeFileSync(collectedPath, JSON.stringify(root, null, 2));
|
|
1217
|
+
const cwd = process.cwd();
|
|
1218
|
+
if (collectedPath.startsWith(cwd)) return collectedPath.slice(cwd.length + 1);
|
|
1219
|
+
return collectedPath;
|
|
1220
|
+
}
|
|
1221
|
+
/**
|
|
1222
|
+
* Collect Round 2 responses in parallel with progress display.
|
|
1223
|
+
*
|
|
1224
|
+
* Port of `collect_round2_parallel()` from round2.rs lines 607-643.
|
|
1225
|
+
*/
|
|
1226
|
+
async function collectRound2Parallel(client, registry, pendingRequests, coordinatorKeys, expectedGroupId, timeout) {
|
|
1227
|
+
const requests = [];
|
|
1228
|
+
for (const [xid, arid] of pendingRequests.iterCollect()) {
|
|
1229
|
+
const name = registry.participant(xid)?.petName() ?? xid.urString();
|
|
1230
|
+
requests.push([
|
|
1231
|
+
xid,
|
|
1232
|
+
arid,
|
|
1233
|
+
name
|
|
1234
|
+
]);
|
|
1235
|
+
}
|
|
1236
|
+
return parallelFetch(client, requests, (envelope, xid) => validateAndExtractRound2Response(envelope, coordinatorKeys, expectedGroupId, xid), parallelFetchConfigWithTimeout(timeout));
|
|
1237
|
+
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Persist Round 2 packages from parallel collection results.
|
|
1240
|
+
*
|
|
1241
|
+
* Port of `persist_round2_packages()` from round2.rs lines 712-758.
|
|
1242
|
+
*/
|
|
1243
|
+
function persistRound2Packages(registryPath, groupId, successes) {
|
|
1244
|
+
const stateDir = groupStateDir(registryPath, groupId.hex());
|
|
1245
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
1246
|
+
const collectedPath = path.join(stateDir, "collected_round2.json");
|
|
1247
|
+
const root = {};
|
|
1248
|
+
for (const [sender, data] of successes) {
|
|
1249
|
+
const senderMap = {};
|
|
1250
|
+
senderMap["response_arid"] = data.nextResponseArid.urString();
|
|
1251
|
+
const packagesJson = {};
|
|
1252
|
+
for (const [recipient, pkg] of data.packages) packagesJson[recipient.urString()] = pkg;
|
|
1253
|
+
senderMap["packages"] = packagesJson;
|
|
1254
|
+
root[sender.urString()] = senderMap;
|
|
1255
|
+
}
|
|
1256
|
+
fs.writeFileSync(collectedPath, JSON.stringify(root, null, 2));
|
|
1257
|
+
const cwd = process.cwd();
|
|
1258
|
+
if (collectedPath.startsWith(cwd)) return collectedPath.slice(cwd.length + 1);
|
|
1259
|
+
return collectedPath;
|
|
1260
|
+
}
|
|
1261
|
+
/**
|
|
1262
|
+
* Update pending requests from parallel collection results.
|
|
1263
|
+
*
|
|
1264
|
+
* Port of `update_pending_for_finalize_from_collection()` from round2.rs lines 761-777.
|
|
1265
|
+
*/
|
|
1266
|
+
function updatePendingForFinalizeFromCollection(registry, registryPath, groupId, successes) {
|
|
1267
|
+
const newPending = new PendingRequests();
|
|
1268
|
+
for (const [xid, data] of successes) newPending.addSendOnly(xid, data.nextResponseArid);
|
|
1269
|
+
const groupRecord = registry.group(groupId);
|
|
1270
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
1271
|
+
groupRecord.setPendingRequests(newPending);
|
|
1272
|
+
registry.save(registryPath);
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Gather packages FOR a specific recipient (from all other senders).
|
|
1276
|
+
*
|
|
1277
|
+
* Port of `gather_packages_for_recipient()` from round2.rs lines 552-571.
|
|
1278
|
+
*/
|
|
1279
|
+
function gatherPackagesForRecipient(recipient, allPackages) {
|
|
1280
|
+
const result = [];
|
|
1281
|
+
for (const [senderUrString, packages] of allPackages) for (const [rcpt, pkg] of packages) if (rcpt.urString() === recipient.urString()) {
|
|
1282
|
+
const { XID: XIDClass } = __require("@bcts/components");
|
|
1283
|
+
const sender = XIDClass.fromURString(senderUrString);
|
|
1284
|
+
result.push([sender, pkg]);
|
|
1285
|
+
}
|
|
1286
|
+
if (result.length === 0) throw new Error(`No round2 packages found for recipient ${recipient.urString()}`);
|
|
1287
|
+
return result;
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Build a finalize request for a participant.
|
|
1291
|
+
*
|
|
1292
|
+
* Port of `build_finalize_request_for_participant()` from round2.rs lines 575-594.
|
|
1293
|
+
*/
|
|
1294
|
+
function buildFinalizeRequestForParticipant(sender, groupId, responseArid, packages) {
|
|
1295
|
+
let request = SealedRequest.new("dkgFinalize", ARID.new(), sender).withParameter("group", groupId).withParameter("responseArid", responseArid);
|
|
1296
|
+
for (const [pkgSender, pkg] of packages) {
|
|
1297
|
+
const encoded = JSON.stringify(pkg);
|
|
1298
|
+
const pkgEnvelope = Envelope.new(encoded).addAssertion("sender", pkgSender);
|
|
1299
|
+
request = request.withParameter("round2Package", pkgEnvelope);
|
|
1300
|
+
}
|
|
1301
|
+
return request;
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Send finalize requests sequentially.
|
|
1305
|
+
*
|
|
1306
|
+
* Port of `send_finalize_requests()` from round2.rs lines 444-550.
|
|
1307
|
+
*/
|
|
1308
|
+
async function sendFinalizeRequests(client, registryPath, registry, coordinator, groupId, collection, preview) {
|
|
1309
|
+
const signerPrivateKeys = coordinator.inceptionPrivateKeys();
|
|
1310
|
+
if (signerPrivateKeys === void 0) throw new Error("Coordinator XID document has no signing keys");
|
|
1311
|
+
const validUntil = new Date(Date.now() + 3600 * 1e3);
|
|
1312
|
+
const participantInfo = [];
|
|
1313
|
+
for (const [xid, sendToArid] of collection.nextResponseArids) {
|
|
1314
|
+
const record = registry.participant(xid);
|
|
1315
|
+
if (record === void 0) throw new Error(`Participant ${xid.urString()} not found in registry`);
|
|
1316
|
+
const collectFromArid = ARID.new();
|
|
1317
|
+
participantInfo.push([
|
|
1318
|
+
xid,
|
|
1319
|
+
record.xidDocument(),
|
|
1320
|
+
sendToArid,
|
|
1321
|
+
collectFromArid
|
|
1322
|
+
]);
|
|
1323
|
+
}
|
|
1324
|
+
if (isVerbose()) console.error(`Sending finalize packages to ${participantInfo.length} participants...`);
|
|
1325
|
+
else console.error();
|
|
1326
|
+
let previewOutput;
|
|
1327
|
+
for (const [xid, recipientDoc, sendToArid, collectFromArid] of participantInfo) {
|
|
1328
|
+
const participantName = registry.participant(xid)?.petName() ?? xid.urString();
|
|
1329
|
+
if (isVerbose()) console.error(`${participantName}...`);
|
|
1330
|
+
const request = buildFinalizeRequestForParticipant(coordinator, groupId, collectFromArid, gatherPackagesForRecipient(xid, collection.packages));
|
|
1331
|
+
if (preview && previewOutput === void 0) previewOutput = [participantName, request.toEnvelope(validUntil, signerPrivateKeys, void 0).urString()];
|
|
1332
|
+
await putWithIndicator(client, sendToArid, request.toEnvelopeForRecipients(validUntil, signerPrivateKeys, [recipientDoc]), participantName, isVerbose());
|
|
1333
|
+
}
|
|
1334
|
+
const newPendingRequests = new PendingRequests();
|
|
1335
|
+
for (const [xid, , , collectFromArid] of participantInfo) newPendingRequests.addCollectOnly(xid, collectFromArid);
|
|
1336
|
+
const groupRecord = registry.group(groupId);
|
|
1337
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
1338
|
+
groupRecord.setPendingRequests(newPendingRequests);
|
|
1339
|
+
registry.save(registryPath);
|
|
1340
|
+
return previewOutput;
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Dispatch finalize requests in parallel.
|
|
1344
|
+
*
|
|
1345
|
+
* Port of `dispatch_finalize_requests_parallel()` from round2.rs lines 780-900.
|
|
1346
|
+
*/
|
|
1347
|
+
async function dispatchFinalizeRequestsParallel(client, registry, registryPath, coordinator, groupId, successes, preview) {
|
|
1348
|
+
const signerPrivateKeys = coordinator.inceptionPrivateKeys();
|
|
1349
|
+
if (signerPrivateKeys === void 0) throw new Error("Coordinator XID document has no signing keys");
|
|
1350
|
+
const validUntil = new Date(Date.now() + 3600 * 1e3);
|
|
1351
|
+
const allPackages = /* @__PURE__ */ new Map();
|
|
1352
|
+
for (const [xid, data] of successes) allPackages.set(xid.urString(), data.packages);
|
|
1353
|
+
const messages = [];
|
|
1354
|
+
const collectArids = [];
|
|
1355
|
+
let previewOutput;
|
|
1356
|
+
for (const [xid, data] of successes) {
|
|
1357
|
+
const record = registry.participant(xid);
|
|
1358
|
+
if (record === void 0) throw new Error(`Participant ${xid.urString()} not found in registry`);
|
|
1359
|
+
const recipientDoc = record.xidDocument();
|
|
1360
|
+
const participantName = record.petName() ?? xid.urString();
|
|
1361
|
+
const collectFromArid = ARID.new();
|
|
1362
|
+
collectArids.push([xid, collectFromArid]);
|
|
1363
|
+
const request = buildFinalizeRequestForParticipant(coordinator, groupId, collectFromArid, gatherPackagesForRecipient(xid, allPackages));
|
|
1364
|
+
if (preview && previewOutput === void 0) previewOutput = [participantName, request.toEnvelope(validUntil, signerPrivateKeys, void 0).urString()];
|
|
1365
|
+
const sealedEnvelope = request.toEnvelopeForRecipients(validUntil, signerPrivateKeys, [recipientDoc]);
|
|
1366
|
+
messages.push([
|
|
1367
|
+
xid,
|
|
1368
|
+
data.nextResponseArid,
|
|
1369
|
+
sealedEnvelope,
|
|
1370
|
+
participantName
|
|
1371
|
+
]);
|
|
1372
|
+
}
|
|
1373
|
+
console.error();
|
|
1374
|
+
const failures = (await parallelSend(client, messages, isVerbose())).filter(([, err]) => err !== null);
|
|
1375
|
+
if (failures.length > 0) {
|
|
1376
|
+
for (const [xid, error] of failures) if (error !== null) console.error(`Failed to send to ${xid.urString()}: ${error.message}`);
|
|
1377
|
+
throw new Error(`Failed to send finalize requests to ${failures.length} participants`);
|
|
1378
|
+
}
|
|
1379
|
+
const newPendingRequests = new PendingRequests();
|
|
1380
|
+
for (const [xid, collectFromArid] of collectArids) newPendingRequests.addCollectOnly(xid, collectFromArid);
|
|
1381
|
+
const groupRecord = registry.group(groupId);
|
|
1382
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
1383
|
+
groupRecord.setPendingRequests(newPendingRequests);
|
|
1384
|
+
registry.save(registryPath);
|
|
1385
|
+
return previewOutput;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Print summary for parallel collection.
|
|
1389
|
+
*
|
|
1390
|
+
* Port of `print_summary_parallel()` from round2.rs lines 903-964.
|
|
1391
|
+
*/
|
|
1392
|
+
function printSummaryParallel(collection, displayPath, preview) {
|
|
1393
|
+
if (collection.rejections.length > 0) {
|
|
1394
|
+
console.error();
|
|
1395
|
+
console.error("Rejections:");
|
|
1396
|
+
for (const [xid, reason] of collection.rejections) console.error(` ${xid.urString()}: ${reason}`);
|
|
1397
|
+
}
|
|
1398
|
+
if (collection.errors.length > 0) {
|
|
1399
|
+
console.error();
|
|
1400
|
+
console.error("Errors:");
|
|
1401
|
+
for (const [xid, error] of collection.errors) console.error(` ${xid.urString()}: ${error}`);
|
|
1402
|
+
}
|
|
1403
|
+
if (collection.timeouts.length > 0) {
|
|
1404
|
+
console.error();
|
|
1405
|
+
console.error("Timeouts:");
|
|
1406
|
+
for (const xid of collection.timeouts) console.error(` ${xid.urString()}`);
|
|
1407
|
+
}
|
|
1408
|
+
if (!collection.allSucceeded()) {
|
|
1409
|
+
console.error();
|
|
1410
|
+
console.error(`Round 2 collection incomplete: ${collection.successes.length} succeeded, ${collection.rejections.length} rejected, ${collection.errors.length} errors, ${collection.timeouts.length} timeouts`);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
if (preview !== void 0) {
|
|
1414
|
+
const [participantName, ur] = preview;
|
|
1415
|
+
if (isVerbose()) {
|
|
1416
|
+
console.error(`# Finalize preview for ${participantName}`);
|
|
1417
|
+
console.error();
|
|
1418
|
+
}
|
|
1419
|
+
console.error(`Collected ${collection.successes.length} Round 2 responses to ${displayPath} and sent ${collection.successes.length} finalize requests.`);
|
|
1420
|
+
console.log(ur);
|
|
1421
|
+
} else if (isVerbose()) {
|
|
1422
|
+
console.error();
|
|
1423
|
+
console.error(`Collected ${collection.successes.length} Round 2 responses to ${displayPath} and sent ${collection.successes.length} finalize requests.`);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1427
|
+
* Execute the DKG coordinator round 2 command.
|
|
1428
|
+
*
|
|
1429
|
+
* Collects Round 2 responses and sends finalize packages.
|
|
1430
|
+
*
|
|
1431
|
+
* Port of `CommandArgs::exec()` from cmd/dkg/coordinator/round2.rs lines 59-203.
|
|
1432
|
+
*/
|
|
1433
|
+
async function round2$4(client, options, cwd) {
|
|
1434
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
1435
|
+
const registry = Registry.load(registryPath);
|
|
1436
|
+
const owner = registry.owner();
|
|
1437
|
+
if (owner === void 0) throw new Error("Registry owner is required");
|
|
1438
|
+
const ownerDoc = owner.xidDocument();
|
|
1439
|
+
const groupId = parseAridUr(options.groupId);
|
|
1440
|
+
const groupRecord = registry.group(groupId);
|
|
1441
|
+
if (groupRecord === void 0) throw new Error(`Group ${options.groupId} not found in registry`);
|
|
1442
|
+
validateCoordinator$2(groupRecord, owner.xid());
|
|
1443
|
+
const pendingRequests = groupRecord.pendingRequests();
|
|
1444
|
+
if (pendingRequests.isEmpty()) throw new Error("No pending requests for this group. Did you run 'frost dkg coordinator round1'?");
|
|
1445
|
+
const coordinatorKeys = ownerDoc.inceptionPrivateKeys();
|
|
1446
|
+
if (coordinatorKeys === void 0) throw new Error("Coordinator XID document has no private keys");
|
|
1447
|
+
if (options.parallel === true) {
|
|
1448
|
+
const collection = await collectRound2Parallel(client, registry, pendingRequests, coordinatorKeys, groupId, options.timeoutSeconds);
|
|
1449
|
+
const displayPath = persistRound2Packages(registryPath, groupId, collection.successes);
|
|
1450
|
+
updatePendingForFinalizeFromCollection(registry, registryPath, groupId, collection.successes);
|
|
1451
|
+
printSummaryParallel(collection, displayPath, await dispatchFinalizeRequestsParallel(client, registry, registryPath, ownerDoc, groupId, collection.successes, options.preview ?? false));
|
|
1452
|
+
return {
|
|
1453
|
+
accepted: collection.successes.length,
|
|
1454
|
+
rejected: collection.rejections.length,
|
|
1455
|
+
errors: collection.errors.length,
|
|
1456
|
+
timeouts: collection.timeouts.length,
|
|
1457
|
+
displayPath
|
|
1458
|
+
};
|
|
1459
|
+
} else {
|
|
1460
|
+
const collection = await collectRound2(client, registryPath, registry, coordinatorKeys, groupId, pendingRequests, options.timeoutSeconds);
|
|
1461
|
+
const preview = await sendFinalizeRequests(client, registryPath, registry, ownerDoc, groupId, collection, options.preview ?? false);
|
|
1462
|
+
if (preview !== void 0) {
|
|
1463
|
+
const [participantName, ur] = preview;
|
|
1464
|
+
if (isVerbose()) {
|
|
1465
|
+
console.error(`# Finalize preview for ${participantName}`);
|
|
1466
|
+
console.error();
|
|
1467
|
+
}
|
|
1468
|
+
console.error(`Collected ${collection.packages.size} Round 2 responses to ${collection.displayPath} and sent ${collection.nextResponseArids.length} finalize requests.`);
|
|
1469
|
+
console.log(ur);
|
|
1470
|
+
} else if (isVerbose()) {
|
|
1471
|
+
console.error();
|
|
1472
|
+
console.error(`Collected ${collection.packages.size} Round 2 responses to ${collection.displayPath} and sent ${collection.nextResponseArids.length} finalize requests.`);
|
|
1473
|
+
}
|
|
1474
|
+
return {
|
|
1475
|
+
accepted: collection.packages.size,
|
|
1476
|
+
rejected: 0,
|
|
1477
|
+
errors: 0,
|
|
1478
|
+
timeouts: 0,
|
|
1479
|
+
displayPath: collection.displayPath
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
//#endregion
|
|
1485
|
+
//#region src/cmd/dkg/coordinator/finalize.ts
|
|
1486
|
+
/**
|
|
1487
|
+
* DKG coordinator finalize command.
|
|
1488
|
+
*
|
|
1489
|
+
* Port of cmd/dkg/coordinator/finalize.rs from frost-hubert-rust.
|
|
1490
|
+
*
|
|
1491
|
+
* @module
|
|
1492
|
+
*/
|
|
1493
|
+
/**
|
|
1494
|
+
* Validate that the owner is the coordinator of the group.
|
|
1495
|
+
*
|
|
1496
|
+
* Port of coordinator check from finalize.rs lines 76-82.
|
|
1497
|
+
*/
|
|
1498
|
+
function validateCoordinator$1(groupRecord, ownerXid) {
|
|
1499
|
+
if (groupRecord.coordinator().xid().urString() !== ownerXid.urString()) throw new Error(`Only the coordinator can collect finalize responses. Coordinator: ${groupRecord.coordinator().xid().urString()}, Owner: ${ownerXid.urString()}`);
|
|
1500
|
+
}
|
|
1501
|
+
/**
|
|
1502
|
+
* Validate envelope and extract finalize data (for parallel fetch).
|
|
1503
|
+
*
|
|
1504
|
+
* Port of `validate_and_extract_finalize_response()` from finalize.rs lines 407-466.
|
|
1505
|
+
*/
|
|
1506
|
+
function validateAndExtractFinalizeResponse(envelope, coordinatorKeys, expectedGroupId, expectedParticipant) {
|
|
1507
|
+
const now = /* @__PURE__ */ new Date();
|
|
1508
|
+
let sealed;
|
|
1509
|
+
try {
|
|
1510
|
+
sealed = SealedResponse.tryFromEncryptedEnvelope(envelope, void 0, now, coordinatorKeys);
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
return { rejected: `Failed to decrypt/parse response: ${err instanceof Error ? err.message : String(err)}` };
|
|
1513
|
+
}
|
|
1514
|
+
if (!sealed.isOk()) try {
|
|
1515
|
+
return { rejected: `Participant reported error: ${sealed.error().optionalObjectForPredicate("reason")?.extractString() ?? "unknown reason"}` };
|
|
1516
|
+
} catch {
|
|
1517
|
+
return { rejected: "Participant reported error: unknown reason" };
|
|
1518
|
+
}
|
|
1519
|
+
let result;
|
|
1520
|
+
try {
|
|
1521
|
+
result = sealed.result();
|
|
1522
|
+
} catch {
|
|
1523
|
+
return { rejected: "Finalize response has no result" };
|
|
1524
|
+
}
|
|
1525
|
+
try {
|
|
1526
|
+
result.checkSubjectUnit();
|
|
1527
|
+
result.checkType("dkgFinalizeResponse");
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
return { rejected: `Invalid response type: ${err instanceof Error ? err.message : String(err)}` };
|
|
1530
|
+
}
|
|
1531
|
+
try {
|
|
1532
|
+
const groupId = parseAridUr(result.objectForPredicate("group").extractString());
|
|
1533
|
+
if (groupId.urString() !== expectedGroupId.urString()) return { rejected: `Group ${groupId.urString()} does not match expected ${expectedGroupId.urString()}` };
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
return { rejected: `Failed to extract group: ${err instanceof Error ? err.message : String(err)}` };
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
const participantStr = result.objectForPredicate("participant").extractString();
|
|
1539
|
+
const { XID: XIDClass } = __require("@bcts/components");
|
|
1540
|
+
const participantXid = XIDClass.fromURString(participantStr);
|
|
1541
|
+
if (participantXid.urString() !== expectedParticipant.urString()) return { rejected: `Participant ${participantXid.urString()} does not match expected ${expectedParticipant.urString()}` };
|
|
1542
|
+
} catch (err) {
|
|
1543
|
+
return { rejected: `Failed to extract participant: ${err instanceof Error ? err.message : String(err)}` };
|
|
1544
|
+
}
|
|
1545
|
+
let keyPackage;
|
|
1546
|
+
let publicKeyPackage;
|
|
1547
|
+
try {
|
|
1548
|
+
const keyJsonStr = result.objectForPredicate("key_package").extractString();
|
|
1549
|
+
keyPackage = JSON.parse(keyJsonStr);
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
return { rejected: `Failed to parse key_package: ${err instanceof Error ? err.message : String(err)}` };
|
|
1552
|
+
}
|
|
1553
|
+
try {
|
|
1554
|
+
const pubJsonStr = result.objectForPredicate("public_key_package").extractString();
|
|
1555
|
+
publicKeyPackage = JSON.parse(pubJsonStr);
|
|
1556
|
+
} catch (err) {
|
|
1557
|
+
return { rejected: `Failed to parse public_key_package: ${err instanceof Error ? err.message : String(err)}` };
|
|
1558
|
+
}
|
|
1559
|
+
return {
|
|
1560
|
+
keyPackage,
|
|
1561
|
+
publicKeyPackage
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Fetch a finalize response sequentially.
|
|
1566
|
+
*
|
|
1567
|
+
* Port of `fetch_finalize_response()` from finalize.rs lines 282-358.
|
|
1568
|
+
*/
|
|
1569
|
+
async function fetchFinalizeResponse(client, responseArid, timeout, coordinatorKeys, expectedGroup, expectedParticipant, participantName) {
|
|
1570
|
+
const envelope = await getWithIndicator(client, responseArid, participantName, timeout, isVerbose());
|
|
1571
|
+
if (envelope === null || envelope === void 0) throw new Error("Finalize response not found in Hubert storage");
|
|
1572
|
+
const result = validateAndExtractFinalizeResponse(envelope, coordinatorKeys, expectedGroup, expectedParticipant);
|
|
1573
|
+
if ("rejected" in result) throw new Error(result.rejected);
|
|
1574
|
+
return {
|
|
1575
|
+
participant: expectedParticipant,
|
|
1576
|
+
keyPackage: result.keyPackage,
|
|
1577
|
+
publicKeyPackage: result.publicKeyPackage
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Collect finalize responses in parallel with progress display.
|
|
1582
|
+
*
|
|
1583
|
+
* Port of `collect_finalize_parallel()` from finalize.rs lines 371-404.
|
|
1584
|
+
*/
|
|
1585
|
+
async function collectFinalizeParallel(client, registry, pendingRequests, coordinatorKeys, expectedGroupId, timeout) {
|
|
1586
|
+
const requests = [];
|
|
1587
|
+
for (const [xid, arid] of pendingRequests.iterCollect()) {
|
|
1588
|
+
const name = registry.participant(xid)?.petName() ?? xid.urString();
|
|
1589
|
+
requests.push([
|
|
1590
|
+
xid,
|
|
1591
|
+
arid,
|
|
1592
|
+
name
|
|
1593
|
+
]);
|
|
1594
|
+
}
|
|
1595
|
+
return parallelFetch(client, requests, (envelope, xid) => validateAndExtractFinalizeResponse(envelope, coordinatorKeys, expectedGroupId, xid), parallelFetchConfigWithTimeout(timeout));
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* Finalize collection results: persist, update registry, print summary.
|
|
1599
|
+
*
|
|
1600
|
+
* Port of `finalize_collection_results()` from finalize.rs lines 469-590.
|
|
1601
|
+
*/
|
|
1602
|
+
function finalizeFinalizeCollectionResults(collection, registryPath, registry, groupId) {
|
|
1603
|
+
if (collection.rejections.length > 0) {
|
|
1604
|
+
console.error();
|
|
1605
|
+
console.error("Rejections:");
|
|
1606
|
+
for (const [xid, reason] of collection.rejections) console.error(` ${xid.urString()}: ${reason}`);
|
|
1607
|
+
}
|
|
1608
|
+
if (collection.errors.length > 0) {
|
|
1609
|
+
console.error();
|
|
1610
|
+
console.error("Errors:");
|
|
1611
|
+
for (const [xid, error] of collection.errors) console.error(` ${xid.urString()}: ${error}`);
|
|
1612
|
+
}
|
|
1613
|
+
if (collection.timeouts.length > 0) {
|
|
1614
|
+
console.error();
|
|
1615
|
+
console.error("Timeouts:");
|
|
1616
|
+
for (const xid of collection.timeouts) console.error(` ${xid.urString()}`);
|
|
1617
|
+
}
|
|
1618
|
+
if (!collection.allSucceeded()) throw new Error(`Finalize collection incomplete: ${collection.successes.length} succeeded, ${collection.rejections.length} rejected, ${collection.errors.length} errors, ${collection.timeouts.length} timeouts`);
|
|
1619
|
+
let groupVerifyingKey;
|
|
1620
|
+
for (const [xid, data] of collection.successes) {
|
|
1621
|
+
const pubKeyPkg = data.publicKeyPackage;
|
|
1622
|
+
if (!pubKeyPkg.verifying_key) throw new Error(`Failed to extract verifying key for ${xid.urString()}: missing verifying_key field`);
|
|
1623
|
+
let signingKey;
|
|
1624
|
+
try {
|
|
1625
|
+
signingKey = signingKeyFromVerifying(hexToBytes(pubKeyPkg.verifying_key));
|
|
1626
|
+
} catch (err) {
|
|
1627
|
+
throw new Error(`Failed to extract verifying key for ${xid.urString()}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1628
|
+
}
|
|
1629
|
+
if (groupVerifyingKey !== void 0) {
|
|
1630
|
+
if (groupVerifyingKey.urString() !== signingKey.urString()) throw new Error(`Group verifying key mismatch for participant ${xid.urString()}`);
|
|
1631
|
+
} else groupVerifyingKey = signingKey;
|
|
1632
|
+
}
|
|
1633
|
+
const stateDir = groupStateDir(registryPath, groupId.hex());
|
|
1634
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
1635
|
+
const collectedPath = path.join(stateDir, "collected_finalize.json");
|
|
1636
|
+
const root = {};
|
|
1637
|
+
for (const [xid, data] of collection.successes) root[xid.urString()] = {
|
|
1638
|
+
key_package: data.keyPackage,
|
|
1639
|
+
public_key_package: data.publicKeyPackage
|
|
1640
|
+
};
|
|
1641
|
+
fs.writeFileSync(collectedPath, JSON.stringify(root, null, 2));
|
|
1642
|
+
const groupRecord = registry.group(groupId);
|
|
1643
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
1644
|
+
if (groupVerifyingKey !== void 0) groupRecord.setVerifyingKey(groupVerifyingKey);
|
|
1645
|
+
groupRecord.clearPendingRequests();
|
|
1646
|
+
registry.save(registryPath);
|
|
1647
|
+
if (isVerbose()) {
|
|
1648
|
+
console.error();
|
|
1649
|
+
console.error(`Collected ${collection.successes.length} finalize responses. Saved to ${collectedPath}`);
|
|
1650
|
+
if (groupVerifyingKey !== void 0) console.error(groupVerifyingKey.urString());
|
|
1651
|
+
} else if (groupVerifyingKey !== void 0) console.log(groupVerifyingKey.urString());
|
|
1652
|
+
return groupVerifyingKey;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Helper to convert hex string to bytes.
|
|
1656
|
+
*/
|
|
1657
|
+
function hexToBytes(hex) {
|
|
1658
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
1659
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1660
|
+
return bytes;
|
|
1661
|
+
}
|
|
1662
|
+
/**
|
|
1663
|
+
* Execute the DKG coordinator finalize command.
|
|
1664
|
+
*
|
|
1665
|
+
* Collects finalize responses (key/public key packages) from all participants.
|
|
1666
|
+
*
|
|
1667
|
+
* Port of `finalize()` from cmd/dkg/coordinator/finalize.rs.
|
|
1668
|
+
*/
|
|
1669
|
+
async function finalize$2(client, options, cwd) {
|
|
1670
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
1671
|
+
const registry = Registry.load(registryPath);
|
|
1672
|
+
const owner = registry.owner();
|
|
1673
|
+
if (owner === void 0) throw new Error("Registry owner is required");
|
|
1674
|
+
const groupId = parseAridUr(options.groupId);
|
|
1675
|
+
const groupRecord = registry.group(groupId);
|
|
1676
|
+
if (groupRecord === void 0) throw new Error(`Group ${options.groupId} not found in registry`);
|
|
1677
|
+
validateCoordinator$1(groupRecord, owner.xid());
|
|
1678
|
+
const pendingRequests = groupRecord.pendingRequests();
|
|
1679
|
+
if (pendingRequests.isEmpty()) throw new Error("No pending requests for this group. Did you run 'frost dkg coordinator finalize send'?");
|
|
1680
|
+
const coordinatorKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
1681
|
+
if (coordinatorKeys === void 0) throw new Error("Coordinator XID document has no private keys");
|
|
1682
|
+
let verifyingKey;
|
|
1683
|
+
let collected = 0;
|
|
1684
|
+
let rejected = 0;
|
|
1685
|
+
let errors = 0;
|
|
1686
|
+
let timeouts = 0;
|
|
1687
|
+
if (options.parallel === true) {
|
|
1688
|
+
const collection = await collectFinalizeParallel(client, registry, pendingRequests, coordinatorKeys, groupId, options.timeoutSeconds);
|
|
1689
|
+
verifyingKey = finalizeFinalizeCollectionResults(collection, registryPath, registry, groupId);
|
|
1690
|
+
collected = collection.successes.length;
|
|
1691
|
+
rejected = collection.rejections.length;
|
|
1692
|
+
errors = collection.errors.length;
|
|
1693
|
+
timeouts = collection.timeouts.length;
|
|
1694
|
+
} else {
|
|
1695
|
+
const collectedEntries = [];
|
|
1696
|
+
const errorEntries = [];
|
|
1697
|
+
let groupVerifyingKey;
|
|
1698
|
+
if (isVerbose()) console.error(`Collecting finalize responses from ${pendingRequests.len()} participants...`);
|
|
1699
|
+
for (const [participantXid, collectFromArid] of pendingRequests.iterCollect()) {
|
|
1700
|
+
const name = registry.participant(participantXid)?.petName() ?? participantXid.urString();
|
|
1701
|
+
try {
|
|
1702
|
+
const entry = await fetchFinalizeResponse(client, collectFromArid, options.timeoutSeconds, coordinatorKeys, groupId, participantXid, name);
|
|
1703
|
+
const pubKeyPkg = entry.publicKeyPackage;
|
|
1704
|
+
if (!pubKeyPkg.verifying_key) throw new Error("missing verifying_key field");
|
|
1705
|
+
const signingKey = signingKeyFromVerifying(hexToBytes(pubKeyPkg.verifying_key));
|
|
1706
|
+
if (groupVerifyingKey !== void 0) {
|
|
1707
|
+
if (groupVerifyingKey.urString() !== signingKey.urString()) {
|
|
1708
|
+
if (isVerbose()) console.error("error: group verifying key mismatch");
|
|
1709
|
+
errorEntries.push([participantXid, "Group verifying key mismatch across responses"]);
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
} else groupVerifyingKey = signingKey;
|
|
1713
|
+
collectedEntries.push(entry);
|
|
1714
|
+
} catch (err) {
|
|
1715
|
+
if (isVerbose()) console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1716
|
+
errorEntries.push([participantXid, err instanceof Error ? err.message : String(err)]);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
if (errorEntries.length > 0) {
|
|
1720
|
+
if (isVerbose()) {
|
|
1721
|
+
console.error();
|
|
1722
|
+
console.error(`Failed to collect from ${errorEntries.length} participants:`);
|
|
1723
|
+
for (const [xid, error] of errorEntries) console.error(` ${xid.urString()}: ${error}`);
|
|
1724
|
+
}
|
|
1725
|
+
throw new Error(`Finalize collection incomplete: ${errorEntries.length} of ${pendingRequests.len()} responses failed`);
|
|
1726
|
+
}
|
|
1727
|
+
const stateDir = groupStateDir(registryPath, groupId.hex());
|
|
1728
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
1729
|
+
const collectedPath = path.join(stateDir, "collected_finalize.json");
|
|
1730
|
+
const root = {};
|
|
1731
|
+
for (const entry of collectedEntries) root[entry.participant.urString()] = {
|
|
1732
|
+
key_package: entry.keyPackage,
|
|
1733
|
+
public_key_package: entry.publicKeyPackage
|
|
1734
|
+
};
|
|
1735
|
+
fs.writeFileSync(collectedPath, JSON.stringify(root, null, 2));
|
|
1736
|
+
const groupRecordMut = registry.group(groupId);
|
|
1737
|
+
if (groupRecordMut === void 0) throw new Error("Group not found in registry");
|
|
1738
|
+
if (groupVerifyingKey !== void 0) groupRecordMut.setVerifyingKey(groupVerifyingKey);
|
|
1739
|
+
groupRecordMut.clearPendingRequests();
|
|
1740
|
+
registry.save(registryPath);
|
|
1741
|
+
if (isVerbose()) {
|
|
1742
|
+
console.error();
|
|
1743
|
+
console.error(`Collected ${collectedEntries.length} finalize responses. Saved to ${collectedPath}`);
|
|
1744
|
+
if (groupVerifyingKey !== void 0) console.error(groupVerifyingKey.urString());
|
|
1745
|
+
} else if (groupVerifyingKey !== void 0) console.log(groupVerifyingKey.urString());
|
|
1746
|
+
verifyingKey = groupVerifyingKey;
|
|
1747
|
+
collected = collectedEntries.length;
|
|
1748
|
+
errors = errorEntries.length;
|
|
1749
|
+
}
|
|
1750
|
+
return {
|
|
1751
|
+
verifyingKey: verifyingKey?.urString() ?? "",
|
|
1752
|
+
collected,
|
|
1753
|
+
rejected,
|
|
1754
|
+
errors,
|
|
1755
|
+
timeouts
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
//#endregion
|
|
1760
|
+
//#region src/cmd/dkg/coordinator/index.ts
|
|
1761
|
+
var coordinator_exports$1 = /* @__PURE__ */ __exportAll({
|
|
1762
|
+
buildFinalizeRequestForParticipant: () => buildFinalizeRequestForParticipant,
|
|
1763
|
+
collectRound2Parallel: () => collectRound2Parallel,
|
|
1764
|
+
dispatchFinalizeRequestsParallel: () => dispatchFinalizeRequestsParallel,
|
|
1765
|
+
finalize: () => finalize$2,
|
|
1766
|
+
invite: () => invite$1,
|
|
1767
|
+
persistRound2Packages: () => persistRound2Packages,
|
|
1768
|
+
round1: () => round1$4,
|
|
1769
|
+
round2: () => round2$4,
|
|
1770
|
+
updatePendingForFinalizeFromCollection: () => updatePendingForFinalizeFromCollection,
|
|
1771
|
+
validateAndExtractRound2Response: () => validateAndExtractRound2Response
|
|
1772
|
+
});
|
|
1773
|
+
|
|
1774
|
+
//#endregion
|
|
1775
|
+
//#region src/cmd/dkg/participant/receive.ts
|
|
1776
|
+
/**
|
|
1777
|
+
* DKG participant receive command.
|
|
1778
|
+
*
|
|
1779
|
+
* Port of cmd/dkg/participant/receive.rs from frost-hubert-rust.
|
|
1780
|
+
*
|
|
1781
|
+
* @module
|
|
1782
|
+
*/
|
|
1783
|
+
/**
|
|
1784
|
+
* Resolve an invite envelope from either storage (ARID) or direct UR.
|
|
1785
|
+
*
|
|
1786
|
+
* Port of `resolve_invite_envelope()` from cmd/dkg/participant/receive.rs lines 122-152.
|
|
1787
|
+
*/
|
|
1788
|
+
async function resolveInviteEnvelope$1(selection, invite, timeout) {
|
|
1789
|
+
if (selection !== void 0) {
|
|
1790
|
+
try {
|
|
1791
|
+
const arid = parseAridUr(invite);
|
|
1792
|
+
const envelope = await getWithIndicator(await createStorageClient(selection), arid, "Invite", timeout, false);
|
|
1793
|
+
if (envelope === null || envelope === void 0) throw new Error("Invite not found in Hubert storage");
|
|
1794
|
+
return envelope;
|
|
1795
|
+
} catch (e) {
|
|
1796
|
+
if (e instanceof Error && e.message.includes("Invite not found in Hubert storage")) throw e;
|
|
1797
|
+
}
|
|
1798
|
+
if (timeout !== void 0) throw new Error("--timeout is only valid when retrieving invites from Hubert");
|
|
1799
|
+
return parseEnvelopeUr(invite);
|
|
1800
|
+
}
|
|
1801
|
+
try {
|
|
1802
|
+
parseAridUr(invite);
|
|
1803
|
+
throw new Error("Hubert storage parameters are required to retrieve invites by ARID");
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
if (e instanceof Error && e.message.includes("Hubert storage parameters are required")) throw e;
|
|
1806
|
+
}
|
|
1807
|
+
return parseEnvelopeUr(invite);
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Decode and validate invite details from an envelope.
|
|
1811
|
+
*
|
|
1812
|
+
* Port of `decode_invite_details()` from cmd/dkg/participant/receive.rs lines 154-256.
|
|
1813
|
+
*/
|
|
1814
|
+
function decodeInviteDetails(invite, now, registry, recipient, expectedSender) {
|
|
1815
|
+
const recipientPrivateKeys = recipient.inceptionPrivateKeys();
|
|
1816
|
+
if (recipientPrivateKeys === void 0) throw new Error("Recipient XID document has no inception private keys");
|
|
1817
|
+
const sealedRequest = SealedRequest.tryFromEnvelope(invite, void 0, now, recipientPrivateKeys);
|
|
1818
|
+
const senderDocument = sealedRequest.sender();
|
|
1819
|
+
if (expectedSender !== void 0) {
|
|
1820
|
+
if (senderDocument.xid().urString() !== expectedSender.xid().urString()) throw new Error("Invite sender does not match expected sender");
|
|
1821
|
+
} else {
|
|
1822
|
+
const senderXid = senderDocument.xid();
|
|
1823
|
+
const knownOwner = registry.owner()?.xidDocument().xid().urString() === senderXid.urString();
|
|
1824
|
+
const knownParticipant = registry.participant(senderXid) !== void 0;
|
|
1825
|
+
if (!knownOwner && !knownParticipant) throw new Error(`Invite sender not found in registry: ${senderXid.urString()}`);
|
|
1826
|
+
}
|
|
1827
|
+
if (!sealedRequest.request().function().equals(Function.fromString("dkgInvite"))) throw new Error("Unexpected invite function");
|
|
1828
|
+
if (sealedRequest.extractObjectForParameter("validUntil") <= now) throw new Error("Invitation expired");
|
|
1829
|
+
const minSigners = sealedRequest.extractObjectForParameter("minSigners");
|
|
1830
|
+
sealedRequest.extractObjectForParameter("charter");
|
|
1831
|
+
sealedRequest.extractObjectForParameter("group");
|
|
1832
|
+
const participantObjects = sealedRequest.objectsForParameter("participant");
|
|
1833
|
+
if (minSigners < 2) throw new Error("min_signers must be at least 2");
|
|
1834
|
+
if (minSigners > participantObjects.length) throw new Error("min_signers exceeds participant count");
|
|
1835
|
+
const participantDocs = [];
|
|
1836
|
+
let responseArid;
|
|
1837
|
+
const recipientXid = recipient.xid();
|
|
1838
|
+
for (const participant of participantObjects) {
|
|
1839
|
+
const xidDocumentEnvelope = participant.tryUnwrap();
|
|
1840
|
+
const xidDocument = XIDDocument.fromEnvelope(xidDocumentEnvelope, void 0, XIDVerifySignature.Inception);
|
|
1841
|
+
if (xidDocument.xid().urString() === recipientXid.urString()) responseArid = participant.objectForPredicate("response_arid").decryptToRecipient(recipientPrivateKeys).extractSubject((cbor) => ARID.fromTaggedCbor(cbor));
|
|
1842
|
+
participantDocs.push(xidDocument);
|
|
1843
|
+
}
|
|
1844
|
+
const invitation = DkgInvitation.fromInvite(invite, now, expectedSender, recipient);
|
|
1845
|
+
if (responseArid === void 0) throw new Error("Invite does not include a response ARID for this recipient");
|
|
1846
|
+
return {
|
|
1847
|
+
invitation,
|
|
1848
|
+
participants: participantDocs
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Resolve a sender XID document from the registry by UR or pet name.
|
|
1853
|
+
*
|
|
1854
|
+
* Port of `resolve_sender()` usage in receive.rs for expected sender.
|
|
1855
|
+
*/
|
|
1856
|
+
function resolveSenderXidDocument$1(registry, raw) {
|
|
1857
|
+
try {
|
|
1858
|
+
const xid = XID.fromURString(raw.trim());
|
|
1859
|
+
const record = registry.participant(xid);
|
|
1860
|
+
if (record) return record.xidDocument();
|
|
1861
|
+
const owner = registry.owner();
|
|
1862
|
+
if (owner?.xid().urString() === xid.urString()) return owner.xidDocument();
|
|
1863
|
+
throw new Error(`Sender with XID ${xid.urString()} not found in registry`);
|
|
1864
|
+
} catch {
|
|
1865
|
+
const result = registry.participantByPetName(raw.trim());
|
|
1866
|
+
if (result) {
|
|
1867
|
+
const [, record] = result;
|
|
1868
|
+
return record.xidDocument();
|
|
1869
|
+
}
|
|
1870
|
+
const owner = registry.owner();
|
|
1871
|
+
if (owner?.petName() === raw.trim()) return owner.xidDocument();
|
|
1872
|
+
throw new Error(`Sender '${raw}' not found in registry`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Execute the DKG participant receive command.
|
|
1877
|
+
*
|
|
1878
|
+
* Fetches and validates a DKG invite from the coordinator.
|
|
1879
|
+
*
|
|
1880
|
+
* Port of `receive()` from cmd/dkg/participant/receive.rs.
|
|
1881
|
+
*/
|
|
1882
|
+
async function receive$1(_client, options, cwd) {
|
|
1883
|
+
if (options.storageSelection === void 0 && options.timeoutSeconds !== void 0) throw new Error("--timeout requires Hubert storage parameters");
|
|
1884
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
1885
|
+
const registry = Registry.load(registryPath);
|
|
1886
|
+
const owner = registry.owner();
|
|
1887
|
+
if (!owner) throw new Error("Registry owner with private keys is required");
|
|
1888
|
+
const expectedSender = options.sender ? resolveSenderXidDocument$1(registry, options.sender) : void 0;
|
|
1889
|
+
const inviteEnvelope = await resolveInviteEnvelope$1(options.storageSelection, options.invite, options.timeoutSeconds);
|
|
1890
|
+
const details = decodeInviteDetails(inviteEnvelope, CborDate.now().datetime(), registry, owner.xidDocument(), expectedSender);
|
|
1891
|
+
const participantNames = participantNamesFromRegistry(registry, details.participants, owner.xid(), owner.petName());
|
|
1892
|
+
const coordinatorName = resolveSenderName$1(registry, details.invitation.sender());
|
|
1893
|
+
const stateDir = dkgStateDir(registryPath, details.invitation.groupId().hex());
|
|
1894
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
1895
|
+
const validUntilStr = details.invitation.validUntil().toString();
|
|
1896
|
+
const receiveState = {
|
|
1897
|
+
group: details.invitation.groupId().urString(),
|
|
1898
|
+
request_id: details.invitation.requestId().urString(),
|
|
1899
|
+
response_arid: details.invitation.responseArid().urString(),
|
|
1900
|
+
valid_until: validUntilStr,
|
|
1901
|
+
min_signers: details.invitation.minSigners(),
|
|
1902
|
+
charter: details.invitation.charter(),
|
|
1903
|
+
sender: details.invitation.sender().xid().urString()
|
|
1904
|
+
};
|
|
1905
|
+
fs.writeFileSync(path.join(stateDir, "receive.json"), JSON.stringify(receiveState, null, 2));
|
|
1906
|
+
let envelopeUr;
|
|
1907
|
+
if (options.noEnvelope !== true) {
|
|
1908
|
+
envelopeUr = inviteEnvelope.urString();
|
|
1909
|
+
console.log(envelopeUr);
|
|
1910
|
+
}
|
|
1911
|
+
if (options.info === true) {
|
|
1912
|
+
console.error(`Charter: ${details.invitation.charter()}`);
|
|
1913
|
+
console.error(`Min signers: ${details.invitation.minSigners()}`);
|
|
1914
|
+
if (coordinatorName !== void 0) console.error(`Coordinator: ${coordinatorName}`);
|
|
1915
|
+
console.error(`Participants: ${participantNames.join(", ")}`);
|
|
1916
|
+
}
|
|
1917
|
+
if (options.verbose === true) {
|
|
1918
|
+
console.log(`Group ID: ${details.invitation.groupId().urString()}`);
|
|
1919
|
+
console.log(`Min signers: ${details.invitation.minSigners()}`);
|
|
1920
|
+
console.log(`Charter: ${details.invitation.charter()}`);
|
|
1921
|
+
console.log(`Valid until: ${String(details.invitation.validUntil())}`);
|
|
1922
|
+
console.log(`Response ARID: ${details.invitation.responseArid().urString()}`);
|
|
1923
|
+
}
|
|
1924
|
+
const resultValidUntilStr = details.invitation.validUntil().toString();
|
|
1925
|
+
return {
|
|
1926
|
+
groupId: details.invitation.groupId().urString(),
|
|
1927
|
+
requestId: details.invitation.requestId().urString(),
|
|
1928
|
+
minSigners: details.invitation.minSigners(),
|
|
1929
|
+
charter: details.invitation.charter(),
|
|
1930
|
+
validUntil: resultValidUntilStr,
|
|
1931
|
+
responseArid: details.invitation.responseArid().urString(),
|
|
1932
|
+
envelopeUr,
|
|
1933
|
+
coordinatorName,
|
|
1934
|
+
participantNames
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
//#endregion
|
|
1939
|
+
//#region src/cmd/dkg/participant/round1.ts
|
|
1940
|
+
/**
|
|
1941
|
+
* DKG participant round 1 command.
|
|
1942
|
+
*
|
|
1943
|
+
* Port of cmd/dkg/participant/round1.rs from frost-hubert-rust.
|
|
1944
|
+
*
|
|
1945
|
+
* @module
|
|
1946
|
+
*/
|
|
1947
|
+
/**
|
|
1948
|
+
* Resolve an invite envelope from either storage (ARID) or direct UR.
|
|
1949
|
+
*
|
|
1950
|
+
* Port of `resolve_invite_envelope()` from cmd/dkg/participant/round1.rs lines 256-288.
|
|
1951
|
+
*/
|
|
1952
|
+
async function resolveInviteEnvelope(selection, invite, timeout) {
|
|
1953
|
+
if (selection !== void 0) {
|
|
1954
|
+
try {
|
|
1955
|
+
const arid = parseAridUr(invite);
|
|
1956
|
+
const envelope = await getWithIndicator(await createStorageClient(selection), arid, "Invite", timeout, false);
|
|
1957
|
+
if (envelope === null || envelope === void 0) throw new Error("Invite not found in Hubert storage");
|
|
1958
|
+
return envelope;
|
|
1959
|
+
} catch (e) {
|
|
1960
|
+
if (e instanceof Error && e.message.includes("Invite not found in Hubert storage")) throw e;
|
|
1961
|
+
}
|
|
1962
|
+
if (timeout !== void 0) throw new Error("--timeout is only valid when retrieving invites from Hubert");
|
|
1963
|
+
return parseEnvelopeUr(invite);
|
|
1964
|
+
}
|
|
1965
|
+
try {
|
|
1966
|
+
parseAridUr(invite);
|
|
1967
|
+
throw new Error("Hubert storage parameters are required to retrieve invites by ARID");
|
|
1968
|
+
} catch (e) {
|
|
1969
|
+
if (e instanceof Error && e.message.includes("Hubert storage parameters are required")) throw e;
|
|
1970
|
+
}
|
|
1971
|
+
return parseEnvelopeUr(invite);
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Build the response body envelope.
|
|
1975
|
+
*
|
|
1976
|
+
* Port of `build_response_body()` from cmd/dkg/participant/round1.rs lines 290-308.
|
|
1977
|
+
*/
|
|
1978
|
+
function buildResponseBody$4(groupId, participant, responseArid, round1Package) {
|
|
1979
|
+
let envelope = Envelope.unit().addType("dkgRound1Response").addAssertion("group", groupId).addAssertion("participant", participant).addAssertion("response_arid", responseArid);
|
|
1980
|
+
if (round1Package !== void 0) {
|
|
1981
|
+
const packageJson = serde.round1PackageToJson(round1Package);
|
|
1982
|
+
const jsonStr = globalThis.JSON.stringify(packageJson);
|
|
1983
|
+
const jsonBytes = new TextEncoder().encode(jsonStr);
|
|
1984
|
+
const jsonWrapper = JSON$1.fromData(jsonBytes);
|
|
1985
|
+
envelope = envelope.addAssertion("round1_package", jsonWrapper);
|
|
1986
|
+
}
|
|
1987
|
+
return envelope;
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Serialize round 1 secret package to JSON-compatible format.
|
|
1991
|
+
*
|
|
1992
|
+
* The @frosts/ed25519 serde module doesn't provide a serializer for SecretPackage,
|
|
1993
|
+
* so we manually serialize it here.
|
|
1994
|
+
*/
|
|
1995
|
+
function serializeRound1SecretPackage(secret) {
|
|
1996
|
+
const serializedCoefficients = secret.coefficients().map((c) => bytesToHex(Ed25519Sha512.serializeScalar(c)));
|
|
1997
|
+
const commitmentCoefficients = secret.commitment.serialize().map((c) => bytesToHex(c));
|
|
1998
|
+
return {
|
|
1999
|
+
header: serde.DEFAULT_HEADER,
|
|
2000
|
+
identifier: bytesToHex(secret.identifier.serialize()),
|
|
2001
|
+
coefficients: serializedCoefficients,
|
|
2002
|
+
commitment: commitmentCoefficients,
|
|
2003
|
+
min_signers: secret.minSigners,
|
|
2004
|
+
max_signers: secret.maxSigners
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Persist round 1 state to disk.
|
|
2009
|
+
*
|
|
2010
|
+
* Port of `persist_round1_state()` from cmd/dkg/participant/round1.rs lines 310-337.
|
|
2011
|
+
*/
|
|
2012
|
+
function persistRound1State(registryPath, groupId, round1Secret, round1Package) {
|
|
2013
|
+
const dir = groupStateDir(registryPath, groupId.hex());
|
|
2014
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2015
|
+
const secretPath = path.join(dir, "round1_secret.json");
|
|
2016
|
+
const packagePath = path.join(dir, "round1_package.json");
|
|
2017
|
+
const secretJson = serializeRound1SecretPackage(round1Secret);
|
|
2018
|
+
const packageJson = serde.round1PackageToJson(round1Package);
|
|
2019
|
+
fs.writeFileSync(secretPath, globalThis.JSON.stringify(secretJson, null, 2));
|
|
2020
|
+
fs.writeFileSync(packagePath, globalThis.JSON.stringify(packageJson, null, 2));
|
|
2021
|
+
return new ContributionPaths({
|
|
2022
|
+
round1Secret: secretPath,
|
|
2023
|
+
round1Package: packagePath,
|
|
2024
|
+
round2Secret: void 0,
|
|
2025
|
+
keyPackage: void 0
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Execute the DKG participant round 1 command.
|
|
2030
|
+
*
|
|
2031
|
+
* Responds to the DKG invite with commitment packages.
|
|
2032
|
+
*
|
|
2033
|
+
* Port of `CommandArgs::exec()` from cmd/dkg/participant/round1.rs lines 66-254.
|
|
2034
|
+
*/
|
|
2035
|
+
async function round1$3(_client, options, cwd) {
|
|
2036
|
+
if (options.storageSelection === void 0 && options.timeoutSeconds !== void 0) throw new Error("--timeout requires Hubert storage parameters");
|
|
2037
|
+
if (options.storageSelection !== void 0 && options.preview === true) throw new Error("--preview cannot be used with Hubert storage options");
|
|
2038
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
2039
|
+
const registry = Registry.load(registryPath);
|
|
2040
|
+
const owner = registry.owner();
|
|
2041
|
+
if (!owner) throw new Error("Registry owner with private keys is required");
|
|
2042
|
+
let expectedSender;
|
|
2043
|
+
if (options.sender !== void 0) expectedSender = resolveSenderXidDocument(registry, options.sender);
|
|
2044
|
+
const nextResponseArid = options.responseArid !== void 0 ? parseAridUr(options.responseArid) : ARID.new();
|
|
2045
|
+
const details = decodeInviteDetails(await resolveInviteEnvelope(options.storageSelection, options.invite, options.timeoutSeconds), CborDate.now().datetime(), registry, owner.xidDocument(), expectedSender);
|
|
2046
|
+
const sortedParticipants = [...details.participants].sort((a, b) => a.xid().urString().localeCompare(b.xid().urString()));
|
|
2047
|
+
const ownerIndex = sortedParticipants.findIndex((doc) => doc.xid().urString() === owner.xid().urString());
|
|
2048
|
+
if (ownerIndex === -1) throw new Error("Invite does not include the registry owner");
|
|
2049
|
+
const identifierIndex = ownerIndex + 1;
|
|
2050
|
+
if (identifierIndex > 65535) throw new Error("Too many participants for identifiers");
|
|
2051
|
+
const identifier = identifierFromU16(identifierIndex);
|
|
2052
|
+
const total = sortedParticipants.length;
|
|
2053
|
+
if (total > 65535) throw new Error("Too many participants for FROST identifiers");
|
|
2054
|
+
const minSigners = details.invitation.minSigners();
|
|
2055
|
+
if (minSigners > 65535) throw new Error("min_signers does not fit into identifier space");
|
|
2056
|
+
const groupParticipants = buildGroupParticipants(registry, owner, sortedParticipants);
|
|
2057
|
+
const coordinator = groupParticipantFromRegistry(registry, owner, details.invitation.sender());
|
|
2058
|
+
const isPosting = options.storageSelection !== void 0;
|
|
2059
|
+
let responseBody;
|
|
2060
|
+
let contributions;
|
|
2061
|
+
if (options.rejectReason === void 0 && isPosting) {
|
|
2062
|
+
const [round1Secret, round1Package] = dkgPart1(identifier, total, minSigners, createRng());
|
|
2063
|
+
contributions = persistRound1State(registryPath, details.invitation.groupId(), round1Secret, round1Package);
|
|
2064
|
+
responseBody = buildResponseBody$4(details.invitation.groupId(), owner.xid(), nextResponseArid, round1Package);
|
|
2065
|
+
const groupRecord = new GroupRecord(details.invitation.charter(), details.invitation.minSigners(), coordinator, groupParticipants);
|
|
2066
|
+
groupRecord.setContributions(contributions);
|
|
2067
|
+
groupRecord.setListeningAtArid(nextResponseArid);
|
|
2068
|
+
registry.recordGroup(details.invitation.groupId(), groupRecord);
|
|
2069
|
+
registry.save(registryPath);
|
|
2070
|
+
} else if (options.rejectReason === void 0) {
|
|
2071
|
+
const [, round1Package] = dkgPart1(identifier, total, minSigners, createRng());
|
|
2072
|
+
responseBody = buildResponseBody$4(details.invitation.groupId(), owner.xid(), nextResponseArid, round1Package);
|
|
2073
|
+
} else responseBody = buildResponseBody$4(details.invitation.groupId(), owner.xid(), nextResponseArid, void 0);
|
|
2074
|
+
const signerPrivateKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
2075
|
+
if (signerPrivateKeys === void 0) throw new Error("Owner XID document has no signing keys");
|
|
2076
|
+
let sealed;
|
|
2077
|
+
if (options.rejectReason !== void 0) {
|
|
2078
|
+
const errorBody = Envelope.new("dkgInviteReject").addAssertion("group", details.invitation.groupId()).addAssertion("response_arid", nextResponseArid).addAssertion("reason", options.rejectReason);
|
|
2079
|
+
sealed = SealedResponse.newFailure(details.invitation.requestId(), owner.xidDocument()).withError(errorBody).withState(nextResponseArid);
|
|
2080
|
+
} else sealed = SealedResponse.newSuccess(details.invitation.requestId(), owner.xidDocument()).withResult(responseBody).withState(nextResponseArid);
|
|
2081
|
+
const peerContinuation = details.invitation.peerContinuation();
|
|
2082
|
+
if (peerContinuation !== void 0) sealed = sealed.withPeerContinuation(peerContinuation);
|
|
2083
|
+
if (options.storageSelection !== void 0) {
|
|
2084
|
+
const responseEnvelope = sealed.toEnvelope(details.invitation.validUntil(), signerPrivateKeys, details.invitation.sender());
|
|
2085
|
+
const responseTarget = details.invitation.responseArid();
|
|
2086
|
+
await putWithIndicator(await createStorageClient(options.storageSelection), responseTarget, responseEnvelope, "Round 1 Response", options.verbose ?? false);
|
|
2087
|
+
if (options.verbose === true) {
|
|
2088
|
+
console.log(`Sent round 1 response`);
|
|
2089
|
+
console.log(`Listening at: ${nextResponseArid.urString()}`);
|
|
2090
|
+
}
|
|
2091
|
+
return {
|
|
2092
|
+
accepted: options.rejectReason === void 0,
|
|
2093
|
+
listeningArid: nextResponseArid.urString()
|
|
2094
|
+
};
|
|
2095
|
+
} else if (options.preview === true) {
|
|
2096
|
+
const envelopeUr = sealed.toEnvelope(void 0, signerPrivateKeys, void 0).urString();
|
|
2097
|
+
console.log(envelopeUr);
|
|
2098
|
+
return {
|
|
2099
|
+
accepted: options.rejectReason === void 0,
|
|
2100
|
+
envelopeUr
|
|
2101
|
+
};
|
|
2102
|
+
} else {
|
|
2103
|
+
const envelopeUr = sealed.toEnvelope(details.invitation.validUntil(), signerPrivateKeys, details.invitation.sender()).urString();
|
|
2104
|
+
console.log(envelopeUr);
|
|
2105
|
+
return {
|
|
2106
|
+
accepted: options.rejectReason === void 0,
|
|
2107
|
+
envelopeUr
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Resolve a sender XID document from the registry by UR or pet name.
|
|
2113
|
+
*/
|
|
2114
|
+
function resolveSenderXidDocument(registry, raw) {
|
|
2115
|
+
try {
|
|
2116
|
+
const xid = XID.fromURString(raw.trim());
|
|
2117
|
+
const record = registry.participant(xid);
|
|
2118
|
+
if (record) return record.xidDocument();
|
|
2119
|
+
const owner = registry.owner();
|
|
2120
|
+
if (owner?.xid().urString() === xid.urString()) return owner.xidDocument();
|
|
2121
|
+
throw new Error(`Sender with XID ${xid.urString()} not found in registry`);
|
|
2122
|
+
} catch {
|
|
2123
|
+
const result = registry.participantByPetName(raw.trim());
|
|
2124
|
+
if (result) {
|
|
2125
|
+
const [, record] = result;
|
|
2126
|
+
return record.xidDocument();
|
|
2127
|
+
}
|
|
2128
|
+
const owner = registry.owner();
|
|
2129
|
+
if (owner?.petName() === raw.trim()) return owner.xidDocument();
|
|
2130
|
+
throw new Error(`Sender '${raw}' not found in registry`);
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
//#endregion
|
|
2135
|
+
//#region src/cmd/dkg/participant/round2.ts
|
|
2136
|
+
/**
|
|
2137
|
+
* DKG participant round 2 command.
|
|
2138
|
+
*
|
|
2139
|
+
* Port of cmd/dkg/participant/round2.rs from frost-hubert-rust.
|
|
2140
|
+
*
|
|
2141
|
+
* @module
|
|
2142
|
+
*/
|
|
2143
|
+
/**
|
|
2144
|
+
* Load persisted round 1 state from disk.
|
|
2145
|
+
*
|
|
2146
|
+
* Port of round1_secret loading from cmd/dkg/participant/round2.rs lines 86-97.
|
|
2147
|
+
*/
|
|
2148
|
+
function loadRound1State(registryPath, groupId) {
|
|
2149
|
+
const packagesDir = groupStateDir(registryPath, groupId.hex());
|
|
2150
|
+
const round1SecretPath = path.join(packagesDir, "round1_secret.json");
|
|
2151
|
+
if (!fs.existsSync(round1SecretPath)) throw new Error(`Round 1 secret not found at ${round1SecretPath}. Did you respond to the invite?`);
|
|
2152
|
+
const secretJson = JSON.parse(fs.readFileSync(round1SecretPath, "utf-8"));
|
|
2153
|
+
const coefficients = secretJson.coefficients.map((hex) => Ed25519Sha512.deserializeScalar(hexToBytes$1(hex)));
|
|
2154
|
+
const commitment = new VerifiableSecretSharingCommitment(Ed25519Sha512, secretJson.commitment.map((hex) => CoefficientCommitment.deserialize(Ed25519Sha512, hexToBytes$1(hex))));
|
|
2155
|
+
const idBytes = hexToBytes$1(secretJson.identifier);
|
|
2156
|
+
let identifierU16 = 1;
|
|
2157
|
+
if (idBytes.length >= 2) identifierU16 = idBytes[0] | idBytes[1] << 8;
|
|
2158
|
+
if (identifierU16 === 0) identifierU16 = 1;
|
|
2159
|
+
const parsedIdentifier = identifierFromU16(identifierU16);
|
|
2160
|
+
const secretPackage = new round1.SecretPackage(Ed25519Sha512, parsedIdentifier, coefficients, commitment, secretJson.min_signers, secretJson.max_signers);
|
|
2161
|
+
const round1PackagePath = path.join(packagesDir, "round1_package.json");
|
|
2162
|
+
const packageJson = JSON.parse(fs.readFileSync(round1PackagePath, "utf-8"));
|
|
2163
|
+
return {
|
|
2164
|
+
secretPackage,
|
|
2165
|
+
ourRound1Package: serde.round1PackageFromJson(packageJson)
|
|
2166
|
+
};
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Validate the round 2 request from the coordinator.
|
|
2170
|
+
*
|
|
2171
|
+
* Port of request validation from cmd/dkg/participant/round2.rs lines 118-158.
|
|
2172
|
+
*/
|
|
2173
|
+
function validateRound2Request(sealedRequest, groupId, expectedCoordinator) {
|
|
2174
|
+
if (!sealedRequest.function().equals(Function.fromString("dkgRound2"))) throw new Error(`Unexpected request function: ${sealedRequest.function().toString()}`);
|
|
2175
|
+
if (sealedRequest.sender().xid().urString() !== expectedCoordinator.urString()) throw new Error(`Unexpected request sender: ${sealedRequest.sender().xid().urString()} (expected coordinator ${expectedCoordinator.urString()})`);
|
|
2176
|
+
const requestGroupIdEnvelope = sealedRequest.objectForParameter("group");
|
|
2177
|
+
if (requestGroupIdEnvelope === void 0) throw new Error("Request missing group parameter");
|
|
2178
|
+
const requestGroupId = requestGroupIdEnvelope.extractSubject((cbor) => ARID.fromTaggedCbor(cbor));
|
|
2179
|
+
if (requestGroupId.urString() !== groupId.urString()) throw new Error(`Request group ID ${requestGroupId.urString()} does not match expected ${groupId.urString()}`);
|
|
2180
|
+
const responseAridEnvelope = sealedRequest.objectForParameter("responseArid");
|
|
2181
|
+
if (responseAridEnvelope === void 0) throw new Error("Request missing responseArid parameter");
|
|
2182
|
+
return responseAridEnvelope.extractSubject((cbor) => ARID.fromTaggedCbor(cbor));
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Extract round 1 packages from the request and convert to Map<Identifier, Package>.
|
|
2186
|
+
*
|
|
2187
|
+
* Port of `extract_round1_packages()` from cmd/dkg/participant/round2.rs lines 291-366.
|
|
2188
|
+
*/
|
|
2189
|
+
function extractRound1Packages(request, groupRecord, ownerXid) {
|
|
2190
|
+
const sortedXids = groupRecord.participants().map((p) => p.xid());
|
|
2191
|
+
const ownerUrString = ownerXid.urString();
|
|
2192
|
+
if (!sortedXids.some((xid) => xid.urString() === ownerUrString)) sortedXids.push(ownerXid);
|
|
2193
|
+
sortedXids.sort((a, b) => a.urString().localeCompare(b.urString()));
|
|
2194
|
+
const deduped = [];
|
|
2195
|
+
for (const xid of sortedXids) if (deduped.length === 0 || deduped[deduped.length - 1].urString() !== xid.urString()) deduped.push(xid);
|
|
2196
|
+
const xidToIdentifier = /* @__PURE__ */ new Map();
|
|
2197
|
+
for (let i = 0; i < deduped.length; i++) {
|
|
2198
|
+
const identifier = identifierFromU16(i + 1);
|
|
2199
|
+
xidToIdentifier.set(deduped[i].urString(), identifier);
|
|
2200
|
+
}
|
|
2201
|
+
const myXidStr = ownerXid.urString();
|
|
2202
|
+
const packages = /* @__PURE__ */ new Map();
|
|
2203
|
+
const packagesByXid = [];
|
|
2204
|
+
const packageEnvelopes = request.objectsForParameter("round1Package");
|
|
2205
|
+
for (const packageEnvelope of packageEnvelopes) {
|
|
2206
|
+
const participantEnvelope = packageEnvelope.objectForPredicate("participant");
|
|
2207
|
+
if (participantEnvelope === void 0) throw new Error("round1Package missing participant predicate");
|
|
2208
|
+
const participantXid = participantEnvelope.extractSubject((cbor) => XID.fromTaggedCbor(cbor));
|
|
2209
|
+
if (participantXid.urString() === myXidStr) continue;
|
|
2210
|
+
const packageJson = packageEnvelope.extractSubject((cbor) => JSON$1.fromTaggedCbor(cbor));
|
|
2211
|
+
const packageData = JSON.parse(new TextDecoder().decode(packageJson.toData()));
|
|
2212
|
+
const pkg = serde.round1PackageFromJson(packageData);
|
|
2213
|
+
const identifier = xidToIdentifier.get(participantXid.urString());
|
|
2214
|
+
if (identifier === void 0) throw new Error(`Unknown participant XID in round1Package: ${participantXid.urString()}`);
|
|
2215
|
+
packages.set(identifier, pkg);
|
|
2216
|
+
packagesByXid.push([participantXid, pkg]);
|
|
2217
|
+
}
|
|
2218
|
+
const expectedPackages = xidToIdentifier.size - 1;
|
|
2219
|
+
if (packages.size !== expectedPackages) throw new Error(`Expected ${expectedPackages} Round 1 packages, found ${packages.size}`);
|
|
2220
|
+
return [packages, packagesByXid];
|
|
2221
|
+
}
|
|
2222
|
+
/**
|
|
2223
|
+
* Build the response body containing Round 2 packages.
|
|
2224
|
+
*
|
|
2225
|
+
* Port of `build_response_body()` from cmd/dkg/participant/round2.rs lines 373-425.
|
|
2226
|
+
*/
|
|
2227
|
+
function buildResponseBody$3(groupId, participantXid, responseArid, round2Packages, groupRecord) {
|
|
2228
|
+
const sortedXids = groupRecord.participants().map((p) => p.xid());
|
|
2229
|
+
const participantUrString = participantXid.urString();
|
|
2230
|
+
if (!sortedXids.some((xid) => xid.urString() === participantUrString)) sortedXids.push(participantXid);
|
|
2231
|
+
sortedXids.sort((a, b) => a.urString().localeCompare(b.urString()));
|
|
2232
|
+
const deduped = [];
|
|
2233
|
+
for (const xid of sortedXids) if (deduped.length === 0 || deduped[deduped.length - 1].urString() !== xid.urString()) deduped.push(xid);
|
|
2234
|
+
const identifierToXid = /* @__PURE__ */ new Map();
|
|
2235
|
+
for (let i = 0; i < deduped.length; i++) {
|
|
2236
|
+
const identifier = identifierFromU16(i + 1);
|
|
2237
|
+
identifierToXid.set(identifierToHex(identifier), deduped[i]);
|
|
2238
|
+
}
|
|
2239
|
+
let envelope = Envelope.unit().addType("dkgRound2Response").addAssertion("group", groupId).addAssertion("participant", participantXid).addAssertion("response_arid", responseArid);
|
|
2240
|
+
for (const [identifier, pkg] of round2Packages) {
|
|
2241
|
+
const idHex = identifierToHex(identifier);
|
|
2242
|
+
const recipientXid = identifierToXid.get(idHex);
|
|
2243
|
+
if (recipientXid === void 0) throw new Error("Unknown identifier in round2_packages");
|
|
2244
|
+
const serialized = serializeDkgRound2Package(pkg);
|
|
2245
|
+
const jsonStr = JSON.stringify(serialized);
|
|
2246
|
+
const jsonBytes = new TextEncoder().encode(jsonStr);
|
|
2247
|
+
const jsonWrapper = JSON$1.fromData(jsonBytes);
|
|
2248
|
+
const packageEnvelope = Envelope.new(jsonWrapper).addAssertion("recipient", recipientXid);
|
|
2249
|
+
envelope = envelope.addAssertion("round2Package", packageEnvelope);
|
|
2250
|
+
}
|
|
2251
|
+
return envelope;
|
|
2252
|
+
}
|
|
2253
|
+
/**
|
|
2254
|
+
* Serialize round 2 secret package to JSON format for persistence.
|
|
2255
|
+
*
|
|
2256
|
+
* The format matches what finalize.ts expects to deserialize.
|
|
2257
|
+
*/
|
|
2258
|
+
function serializeRound2SecretPackage(secret, participantIndex) {
|
|
2259
|
+
const commitmentCoefficients = secret.commitment.serialize().map((c) => bytesToHex(c));
|
|
2260
|
+
const secretShare = bytesToHex(Ed25519Sha512.serializeScalar(secret.secretShare()));
|
|
2261
|
+
return {
|
|
2262
|
+
identifier: participantIndex,
|
|
2263
|
+
commitment: { coefficients: commitmentCoefficients },
|
|
2264
|
+
secretShare,
|
|
2265
|
+
minSigners: secret.minSigners,
|
|
2266
|
+
maxSigners: secret.maxSigners
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Persist round 2 state to disk.
|
|
2271
|
+
*
|
|
2272
|
+
* Port of round 2 secret persistence from cmd/dkg/participant/round2.rs lines 229-251.
|
|
2273
|
+
*/
|
|
2274
|
+
function persistRound2State(registryPath, groupId, round2Secret, round1PackagesByXid, participantIndex) {
|
|
2275
|
+
const packagesDir = groupStateDir(registryPath, groupId.hex());
|
|
2276
|
+
fs.mkdirSync(packagesDir, { recursive: true });
|
|
2277
|
+
const round2SecretPath = path.join(packagesDir, "round2_secret.json");
|
|
2278
|
+
const round2SecretJson = serializeRound2SecretPackage(round2Secret, participantIndex);
|
|
2279
|
+
fs.writeFileSync(round2SecretPath, JSON.stringify(round2SecretJson, null, 2));
|
|
2280
|
+
const round1PackagesPath = path.join(packagesDir, "collected_round1.json");
|
|
2281
|
+
const round1Json = {};
|
|
2282
|
+
for (const [xid, pkg] of round1PackagesByXid) {
|
|
2283
|
+
const packageJson = serde.round1PackageToJson(pkg);
|
|
2284
|
+
round1Json[xid.urString()] = packageJson;
|
|
2285
|
+
}
|
|
2286
|
+
fs.writeFileSync(round1PackagesPath, JSON.stringify(round1Json, null, 2));
|
|
2287
|
+
return round2SecretPath;
|
|
2288
|
+
}
|
|
2289
|
+
/**
|
|
2290
|
+
* Execute the DKG participant round 2 command.
|
|
2291
|
+
*
|
|
2292
|
+
* Responds to the Round 2 request from the coordinator, runs FROST DKG part2
|
|
2293
|
+
* to generate Round 2 packages, and posts the response back.
|
|
2294
|
+
*
|
|
2295
|
+
* Port of `CommandArgs::exec()` from cmd/dkg/participant/round2.rs lines 55-288.
|
|
2296
|
+
*/
|
|
2297
|
+
async function round2$3(_client, options, cwd) {
|
|
2298
|
+
if (options.storageSelection === void 0) throw new Error("Hubert storage is required for round2");
|
|
2299
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
2300
|
+
const registry = Registry.load(registryPath);
|
|
2301
|
+
const owner = registry.owner();
|
|
2302
|
+
if (owner === void 0) throw new Error("Registry owner is required");
|
|
2303
|
+
const groupId = parseAridUr(options.groupId);
|
|
2304
|
+
const groupRecord = registry.group(groupId);
|
|
2305
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
2306
|
+
const listeningAtArid = groupRecord.listeningAtArid();
|
|
2307
|
+
if (listeningAtArid === void 0) throw new Error("No listening ARID for this group. Did you respond to the invite?");
|
|
2308
|
+
const round1State = loadRound1State(registryPath, groupId);
|
|
2309
|
+
if (isVerbose() || options.verbose === true) console.error("Fetching Round 2 request from Hubert...");
|
|
2310
|
+
const client = await createStorageClient(options.storageSelection);
|
|
2311
|
+
const requestEnvelope = await getWithIndicator(client, listeningAtArid, "Round 2 request", options.timeoutSeconds, options.verbose ?? false);
|
|
2312
|
+
if (requestEnvelope === null || requestEnvelope === void 0) throw new Error("Round 2 request not found in Hubert storage");
|
|
2313
|
+
const ownerPrivateKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
2314
|
+
if (ownerPrivateKeys === void 0) throw new Error("Owner XID document has no private keys");
|
|
2315
|
+
const now = CborDate.now().datetime();
|
|
2316
|
+
const sealedRequest = SealedRequest.tryFromEnvelope(requestEnvelope, void 0, now, ownerPrivateKeys);
|
|
2317
|
+
const responseArid = validateRound2Request(sealedRequest, groupId, groupRecord.coordinator().xid());
|
|
2318
|
+
const [round1Packages, round1PackagesByXid] = extractRound1Packages(sealedRequest, groupRecord, owner.xid());
|
|
2319
|
+
if (isVerbose() || options.verbose === true) console.error(`Received ${round1Packages.size} Round 1 packages. Running DKG part2...`);
|
|
2320
|
+
const nextResponseArid = ARID.new();
|
|
2321
|
+
const round1PackagesHex = /* @__PURE__ */ new Map();
|
|
2322
|
+
for (const [id, pkg] of round1Packages) round1PackagesHex.set(identifierToHex(id), pkg);
|
|
2323
|
+
const [round2Secret, round2Packages] = dkgPart2(round1State.secretPackage, round1PackagesHex);
|
|
2324
|
+
if (isVerbose() || options.verbose === true) console.error(`Generated ${round2Packages.size} Round 2 packages.`);
|
|
2325
|
+
const round2PackagesById = /* @__PURE__ */ new Map();
|
|
2326
|
+
for (const [idHex, pkg] of round2Packages) for (const [id] of round1Packages) if (identifierToHex(id) === idHex) {
|
|
2327
|
+
round2PackagesById.set(id, pkg);
|
|
2328
|
+
break;
|
|
2329
|
+
}
|
|
2330
|
+
const responseBody = buildResponseBody$3(groupId, owner.xid(), nextResponseArid, round2PackagesById, groupRecord);
|
|
2331
|
+
const signerPrivateKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
2332
|
+
if (signerPrivateKeys === void 0) throw new Error("Owner XID document has no signing keys");
|
|
2333
|
+
const coordinatorXid = groupRecord.coordinator().xid();
|
|
2334
|
+
const coordinatorRecord = registry.participant(coordinatorXid);
|
|
2335
|
+
let coordinatorDoc;
|
|
2336
|
+
if (coordinatorRecord !== void 0) coordinatorDoc = coordinatorRecord.xidDocument();
|
|
2337
|
+
else if (owner.xid().urString() === coordinatorXid.urString()) coordinatorDoc = owner.xidDocument();
|
|
2338
|
+
else throw new Error(`Coordinator ${coordinatorXid.urString()} not found in registry`);
|
|
2339
|
+
const peerContinuation = sealedRequest.peerContinuation();
|
|
2340
|
+
let sealed = SealedResponse.newSuccess(sealedRequest.id(), owner.xidDocument()).withResult(responseBody);
|
|
2341
|
+
if (peerContinuation !== void 0) sealed = sealed.withPeerContinuation(peerContinuation);
|
|
2342
|
+
if (options.preview === true) {
|
|
2343
|
+
const envelopeUr = sealed.toEnvelope(void 0, signerPrivateKeys, void 0).urString();
|
|
2344
|
+
console.log(envelopeUr);
|
|
2345
|
+
return {
|
|
2346
|
+
listeningArid: nextResponseArid.urString(),
|
|
2347
|
+
envelopeUr
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
const sortedXids = groupRecord.participants().map((p) => p.xid().urString());
|
|
2351
|
+
const ownerXidStr = owner.xid().urString();
|
|
2352
|
+
if (!sortedXids.includes(ownerXidStr)) sortedXids.push(ownerXidStr);
|
|
2353
|
+
sortedXids.sort();
|
|
2354
|
+
const round2SecretPath = persistRound2State(registryPath, groupId, round2Secret, round1PackagesByXid, sortedXids.indexOf(ownerXidStr) + 1);
|
|
2355
|
+
await putWithIndicator(client, responseArid, sealed.toEnvelope(void 0, signerPrivateKeys, coordinatorDoc), "Round 2 Response", options.verbose ?? false);
|
|
2356
|
+
const updatedGroupRecord = registry.group(groupId);
|
|
2357
|
+
if (updatedGroupRecord !== void 0) {
|
|
2358
|
+
const contributions = updatedGroupRecord.contributions();
|
|
2359
|
+
contributions.round2Secret = round2SecretPath;
|
|
2360
|
+
updatedGroupRecord.setContributions(contributions);
|
|
2361
|
+
updatedGroupRecord.setListeningAtArid(nextResponseArid);
|
|
2362
|
+
registry.save(registryPath);
|
|
2363
|
+
}
|
|
2364
|
+
if (isVerbose() || options.verbose === true) console.error(`Posted Round 2 response to ${responseArid.urString()}`);
|
|
2365
|
+
return { listeningArid: nextResponseArid.urString() };
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
//#endregion
|
|
2369
|
+
//#region src/cmd/dkg/participant/finalize.ts
|
|
2370
|
+
/**
|
|
2371
|
+
* DKG participant finalize command.
|
|
2372
|
+
*
|
|
2373
|
+
* Port of cmd/dkg/participant/finalize.rs from frost-hubert-rust.
|
|
2374
|
+
*
|
|
2375
|
+
* @module
|
|
2376
|
+
*/
|
|
2377
|
+
/**
|
|
2378
|
+
* Load persisted round 2 state from disk.
|
|
2379
|
+
*
|
|
2380
|
+
* Port of round2_secret loading from cmd/dkg/participant/finalize.rs lines 82-106.
|
|
2381
|
+
*/
|
|
2382
|
+
function loadRound2State(registryPath, groupId) {
|
|
2383
|
+
const stateDir = groupStateDir(registryPath, groupId.hex());
|
|
2384
|
+
const round2SecretPath = path.join(stateDir, "round2_secret.json");
|
|
2385
|
+
if (!fs.existsSync(round2SecretPath)) throw new Error(`Round 2 secret not found at ${round2SecretPath}. Did you run round2?`);
|
|
2386
|
+
const secretJson = JSON.parse(fs.readFileSync(round2SecretPath, "utf-8"));
|
|
2387
|
+
const identifier = identifierFromU16(secretJson.identifier);
|
|
2388
|
+
const commitment = new VerifiableSecretSharingCommitment(Ed25519Sha512, secretJson.commitment.coefficients.map((hex) => CoefficientCommitment.deserialize(Ed25519Sha512, hexToBytes$1(hex))));
|
|
2389
|
+
const secretShareScalar = Ed25519Sha512.deserializeScalar(hexToBytes$1(secretJson.secretShare));
|
|
2390
|
+
const secretPackage = new round2.SecretPackage(Ed25519Sha512, identifier, commitment, secretShareScalar, secretJson.minSigners, secretJson.maxSigners);
|
|
2391
|
+
const round1Path = path.join(stateDir, "collected_round1.json");
|
|
2392
|
+
if (!fs.existsSync(round1Path)) throw new Error(`Round 1 packages not found at ${round1Path}. Did you receive earlier phases?`);
|
|
2393
|
+
const round1Json = JSON.parse(fs.readFileSync(round1Path, "utf-8"));
|
|
2394
|
+
const round1Packages = /* @__PURE__ */ new Map();
|
|
2395
|
+
for (const [xidStr, value] of Object.entries(round1Json)) {
|
|
2396
|
+
const packageJson = value;
|
|
2397
|
+
const pkg = serde.round1PackageFromJson(packageJson);
|
|
2398
|
+
round1Packages.set(xidStr, pkg);
|
|
2399
|
+
}
|
|
2400
|
+
return {
|
|
2401
|
+
secretPackage,
|
|
2402
|
+
round1Packages
|
|
2403
|
+
};
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Validate the finalize request from the coordinator.
|
|
2407
|
+
*
|
|
2408
|
+
* Port of request validation from cmd/dkg/participant/finalize.rs lines 139-161.
|
|
2409
|
+
*/
|
|
2410
|
+
function validateFinalizeRequest(sealedRequest, groupId, expectedCoordinator) {
|
|
2411
|
+
if (!sealedRequest.function().equals(Function.fromString("dkgFinalize"))) throw new Error(`Unexpected request function: ${sealedRequest.function().toString()}`);
|
|
2412
|
+
if (sealedRequest.sender().xid().urString() !== expectedCoordinator.urString()) throw new Error(`Unexpected request sender: ${sealedRequest.sender().xid().urString()} (expected coordinator ${expectedCoordinator.urString()})`);
|
|
2413
|
+
const requestGroupIdEnvelope = sealedRequest.objectForParameter("group");
|
|
2414
|
+
if (requestGroupIdEnvelope === void 0) throw new Error("Request missing group parameter");
|
|
2415
|
+
const requestGroupId = requestGroupIdEnvelope.extractSubject((cbor) => ARID.fromTaggedCbor(cbor));
|
|
2416
|
+
if (requestGroupId.urString() !== groupId.urString()) throw new Error(`Request group ID ${requestGroupId.urString()} does not match expected ${groupId.urString()}`);
|
|
2417
|
+
const responseAridEnvelope = sealedRequest.objectForParameter("responseArid");
|
|
2418
|
+
if (responseAridEnvelope === void 0) throw new Error("Request missing responseArid parameter");
|
|
2419
|
+
return responseAridEnvelope.extractSubject((cbor) => ARID.fromTaggedCbor(cbor));
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Extract round 2 packages from the finalize request.
|
|
2423
|
+
*
|
|
2424
|
+
* Port of round2 package extraction from cmd/dkg/participant/finalize.rs lines 209-229.
|
|
2425
|
+
*/
|
|
2426
|
+
function extractFinalizePackages(request, groupRecord, ownerXid) {
|
|
2427
|
+
const sortedXids = groupRecord.participants().map((p) => p.xid());
|
|
2428
|
+
const ownerUrString = ownerXid.urString();
|
|
2429
|
+
if (!sortedXids.some((xid) => xid.urString() === ownerUrString)) sortedXids.push(ownerXid);
|
|
2430
|
+
sortedXids.sort((a, b) => a.urString().localeCompare(b.urString()));
|
|
2431
|
+
const deduped = [];
|
|
2432
|
+
for (const xid of sortedXids) if (deduped.length === 0 || deduped[deduped.length - 1].urString() !== xid.urString()) deduped.push(xid);
|
|
2433
|
+
const xidToIdentifier = /* @__PURE__ */ new Map();
|
|
2434
|
+
for (let i = 0; i < deduped.length; i++) {
|
|
2435
|
+
const identifier = identifierFromU16(i + 1);
|
|
2436
|
+
xidToIdentifier.set(deduped[i].urString(), identifier);
|
|
2437
|
+
}
|
|
2438
|
+
const myXidStr = ownerXid.urString();
|
|
2439
|
+
const packages = /* @__PURE__ */ new Map();
|
|
2440
|
+
const packageEnvelopes = request.objectsForParameter("round2Package");
|
|
2441
|
+
for (const packageEnvelope of packageEnvelopes) {
|
|
2442
|
+
const senderEnvelope = packageEnvelope.objectForPredicate("sender");
|
|
2443
|
+
if (senderEnvelope === void 0) throw new Error("round2Package missing sender predicate");
|
|
2444
|
+
const senderXid = senderEnvelope.extractSubject((cbor) => XID.fromTaggedCbor(cbor));
|
|
2445
|
+
if (senderXid.urString() === myXidStr) continue;
|
|
2446
|
+
const identifier = xidToIdentifier.get(senderXid.urString());
|
|
2447
|
+
if (identifier === void 0) throw new Error(`Unknown sender XID in round2Package: ${senderXid.urString()}`);
|
|
2448
|
+
const packageJson = packageEnvelope.extractSubject((cbor) => JSON$1.fromTaggedCbor(cbor));
|
|
2449
|
+
const packageData = JSON.parse(new TextDecoder().decode(packageJson.toData()));
|
|
2450
|
+
const pkg = serde.round2PackageFromJson(packageData);
|
|
2451
|
+
packages.set(identifierToHex(identifier), pkg);
|
|
2452
|
+
}
|
|
2453
|
+
return packages;
|
|
2454
|
+
}
|
|
2455
|
+
/**
|
|
2456
|
+
* Build the response body for the finalize response.
|
|
2457
|
+
*
|
|
2458
|
+
* Port of `build_response_body()` from cmd/dkg/participant/finalize.rs lines 344-359.
|
|
2459
|
+
*/
|
|
2460
|
+
function buildResponseBody$2(groupId, participantXid, keyPackage, publicKeyPackage) {
|
|
2461
|
+
const keyPackageJson = serializeKeyPackage(keyPackage);
|
|
2462
|
+
const publicKeyPackageJson = serializePublicKeyPackage(publicKeyPackage);
|
|
2463
|
+
const keyJsonBytes = new TextEncoder().encode(JSON.stringify(keyPackageJson));
|
|
2464
|
+
const keyJsonWrapper = JSON$1.fromData(keyJsonBytes);
|
|
2465
|
+
const pubJsonBytes = new TextEncoder().encode(JSON.stringify(publicKeyPackageJson));
|
|
2466
|
+
const pubJsonWrapper = JSON$1.fromData(pubJsonBytes);
|
|
2467
|
+
return Envelope.unit().addType("dkgFinalizeResponse").addAssertion("group", groupId).addAssertion("participant", participantXid).addAssertion("key_package", keyJsonWrapper).addAssertion("public_key_package", pubJsonWrapper);
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Persist finalize state (key packages) to disk.
|
|
2471
|
+
*
|
|
2472
|
+
* Port of key package persistence from cmd/dkg/participant/finalize.rs lines 251-257.
|
|
2473
|
+
*/
|
|
2474
|
+
function persistFinalizeState(registryPath, groupId, keyPackage, publicKeyPackage) {
|
|
2475
|
+
const stateDir = groupStateDir(registryPath, groupId.hex());
|
|
2476
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
2477
|
+
const serializedKeyPackage = serializeKeyPackage(keyPackage);
|
|
2478
|
+
const keyPackagePath = path.join(stateDir, "key_package.json");
|
|
2479
|
+
fs.writeFileSync(keyPackagePath, JSON.stringify(serializedKeyPackage, null, 2));
|
|
2480
|
+
const serializedPublicKeyPackage = serializePublicKeyPackage(publicKeyPackage);
|
|
2481
|
+
const publicKeyPackagePath = path.join(stateDir, "public_key_package.json");
|
|
2482
|
+
fs.writeFileSync(publicKeyPackagePath, JSON.stringify(serializedPublicKeyPackage, null, 2));
|
|
2483
|
+
return {
|
|
2484
|
+
keyPackagePath,
|
|
2485
|
+
publicKeyPackagePath
|
|
2486
|
+
};
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Execute the DKG participant finalize command.
|
|
2490
|
+
*
|
|
2491
|
+
* Responds to the finalize request from the coordinator, runs FROST DKG part3
|
|
2492
|
+
* to generate the final key package, and posts the response back.
|
|
2493
|
+
*
|
|
2494
|
+
* Port of `CommandArgs::exec()` from cmd/dkg/participant/finalize.rs lines 52-341.
|
|
2495
|
+
*/
|
|
2496
|
+
async function finalize$1(_client, options, cwd) {
|
|
2497
|
+
if (options.storageSelection === void 0) throw new Error("Hubert storage is required for finalize respond");
|
|
2498
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
2499
|
+
const registry = Registry.load(registryPath);
|
|
2500
|
+
const owner = registry.owner();
|
|
2501
|
+
if (owner === void 0) throw new Error("Registry owner is required");
|
|
2502
|
+
const groupId = parseAridUr(options.groupId);
|
|
2503
|
+
const groupRecord = registry.group(groupId);
|
|
2504
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
2505
|
+
const listeningAtArid = groupRecord.listeningAtArid();
|
|
2506
|
+
if (listeningAtArid === void 0) throw new Error("No listening ARID for this group. Did you receive finalize send?");
|
|
2507
|
+
const round2State = loadRound2State(registryPath, groupId);
|
|
2508
|
+
if (isVerbose() || options.verbose === true) console.error("Fetching finalize request from Hubert...");
|
|
2509
|
+
const client = await createStorageClient(options.storageSelection);
|
|
2510
|
+
const requestEnvelope = await getWithIndicator(client, listeningAtArid, "Finalize request", options.timeoutSeconds, options.verbose ?? false);
|
|
2511
|
+
if (requestEnvelope === null || requestEnvelope === void 0) throw new Error("Finalize request not found in Hubert storage");
|
|
2512
|
+
const ownerPrivateKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
2513
|
+
if (ownerPrivateKeys === void 0) throw new Error("Owner XID document has no private keys");
|
|
2514
|
+
const now = CborDate.now().datetime();
|
|
2515
|
+
const sealedRequest = SealedRequest.tryFromEnvelope(requestEnvelope, void 0, now, ownerPrivateKeys);
|
|
2516
|
+
const responseArid = validateFinalizeRequest(sealedRequest, groupId, groupRecord.coordinator().xid());
|
|
2517
|
+
const sortedXids = groupRecord.participants().map((p) => p.xid());
|
|
2518
|
+
const ownerUrString = owner.xid().urString();
|
|
2519
|
+
if (!sortedXids.some((xid) => xid.urString() === ownerUrString)) sortedXids.push(owner.xid());
|
|
2520
|
+
sortedXids.sort((a, b) => a.urString().localeCompare(b.urString()));
|
|
2521
|
+
const deduped = [];
|
|
2522
|
+
for (const xid of sortedXids) if (deduped.length === 0 || deduped[deduped.length - 1].urString() !== xid.urString()) deduped.push(xid);
|
|
2523
|
+
const xidToIdentifier = /* @__PURE__ */ new Map();
|
|
2524
|
+
for (let i = 0; i < deduped.length; i++) {
|
|
2525
|
+
const identifier = identifierFromU16(i + 1);
|
|
2526
|
+
xidToIdentifier.set(deduped[i].urString(), identifier);
|
|
2527
|
+
}
|
|
2528
|
+
const round1PackagesById = /* @__PURE__ */ new Map();
|
|
2529
|
+
for (const [xidStr, pkg] of round2State.round1Packages) {
|
|
2530
|
+
if (xidStr === ownerUrString) continue;
|
|
2531
|
+
const identifier = xidToIdentifier.get(xidStr);
|
|
2532
|
+
if (identifier === void 0) throw new Error(`Unknown participant XID ${xidStr}`);
|
|
2533
|
+
round1PackagesById.set(identifierToHex(identifier), pkg);
|
|
2534
|
+
}
|
|
2535
|
+
const round2PackagesById = extractFinalizePackages(sealedRequest, groupRecord, owner.xid());
|
|
2536
|
+
if (isVerbose() || options.verbose === true) console.error(`Received ${round2PackagesById.size} Round 2 packages. Running DKG part3...`);
|
|
2537
|
+
const [keyPackage, publicKeyPackage] = await dkgPart3(round2State.secretPackage, round1PackagesById, round2PackagesById);
|
|
2538
|
+
const verifyingKeyBytes = publicKeyPackage.verifyingKey;
|
|
2539
|
+
const groupVerifyingKey = signingKeyFromVerifying(verifyingKeyBytes);
|
|
2540
|
+
if (isVerbose() || options.verbose === true) console.error("Generated key package and public key package.");
|
|
2541
|
+
const { keyPackagePath, publicKeyPackagePath } = persistFinalizeState(registryPath, groupId, keyPackage, publicKeyPackage);
|
|
2542
|
+
const responseBody = buildResponseBody$2(groupId, owner.xid(), keyPackage, publicKeyPackage);
|
|
2543
|
+
const signerPrivateKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
2544
|
+
if (signerPrivateKeys === void 0) throw new Error("Owner XID document has no signing keys");
|
|
2545
|
+
const coordinatorXid = groupRecord.coordinator().xid();
|
|
2546
|
+
const coordinatorRecord = registry.participant(coordinatorXid);
|
|
2547
|
+
let coordinatorDoc;
|
|
2548
|
+
if (coordinatorRecord !== void 0) coordinatorDoc = coordinatorRecord.xidDocument();
|
|
2549
|
+
else if (owner.xid().urString() === coordinatorXid.urString()) coordinatorDoc = owner.xidDocument();
|
|
2550
|
+
else throw new Error(`Coordinator ${coordinatorXid.urString()} not found in registry`);
|
|
2551
|
+
const peerContinuation = sealedRequest.peerContinuation();
|
|
2552
|
+
let sealed = SealedResponse.newSuccess(sealedRequest.id(), owner.xidDocument()).withResult(responseBody);
|
|
2553
|
+
if (peerContinuation !== void 0) sealed = sealed.withPeerContinuation(peerContinuation);
|
|
2554
|
+
if (options.preview === true) {
|
|
2555
|
+
if (isVerbose() || options.verbose === true) {
|
|
2556
|
+
const verifyingKeyWithUrString = groupVerifyingKey;
|
|
2557
|
+
if (typeof verifyingKeyWithUrString.urString === "function") console.error(verifyingKeyWithUrString.urString());
|
|
2558
|
+
}
|
|
2559
|
+
const unsealedEnvelope = sealed.toEnvelope(void 0, signerPrivateKeys, void 0);
|
|
2560
|
+
console.log(unsealedEnvelope.urString());
|
|
2561
|
+
return {
|
|
2562
|
+
verifyingKey: bytesToHex(verifyingKeyBytes),
|
|
2563
|
+
keyPackagePath,
|
|
2564
|
+
publicKeyPackagePath
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
await putWithIndicator(client, responseArid, sealed.toEnvelope(void 0, signerPrivateKeys, coordinatorDoc), "Finalize Response", options.verbose ?? false);
|
|
2568
|
+
const updatedGroupRecord = registry.group(groupId);
|
|
2569
|
+
if (updatedGroupRecord !== void 0) {
|
|
2570
|
+
const contributions = updatedGroupRecord.contributions();
|
|
2571
|
+
contributions.keyPackage = keyPackagePath;
|
|
2572
|
+
updatedGroupRecord.setContributions(contributions);
|
|
2573
|
+
updatedGroupRecord.clearListeningAtArid();
|
|
2574
|
+
const recordWithVerifyingKey = updatedGroupRecord;
|
|
2575
|
+
if (typeof recordWithVerifyingKey.setVerifyingKey === "function") recordWithVerifyingKey.setVerifyingKey(groupVerifyingKey);
|
|
2576
|
+
registry.save(registryPath);
|
|
2577
|
+
}
|
|
2578
|
+
const verifyingKeyHex = bytesToHex(verifyingKeyBytes);
|
|
2579
|
+
if (isVerbose() || options.verbose === true) {
|
|
2580
|
+
console.error(`Posted finalize response to ${responseArid.urString()}`);
|
|
2581
|
+
const verifyingKeyWithUrString = groupVerifyingKey;
|
|
2582
|
+
if (typeof verifyingKeyWithUrString.urString === "function") console.error(verifyingKeyWithUrString.urString());
|
|
2583
|
+
} else {
|
|
2584
|
+
const verifyingKeyWithUrString = groupVerifyingKey;
|
|
2585
|
+
if (typeof verifyingKeyWithUrString.urString === "function") console.log(verifyingKeyWithUrString.urString());
|
|
2586
|
+
}
|
|
2587
|
+
return {
|
|
2588
|
+
verifyingKey: verifyingKeyHex,
|
|
2589
|
+
keyPackagePath,
|
|
2590
|
+
publicKeyPackagePath
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
//#endregion
|
|
2595
|
+
//#region src/cmd/dkg/participant/index.ts
|
|
2596
|
+
var participant_exports$1 = /* @__PURE__ */ __exportAll({
|
|
2597
|
+
decodeInviteDetails: () => decodeInviteDetails,
|
|
2598
|
+
finalize: () => finalize$1,
|
|
2599
|
+
receive: () => receive$1,
|
|
2600
|
+
resolveInviteEnvelope: () => resolveInviteEnvelope$1,
|
|
2601
|
+
round1: () => round1$3,
|
|
2602
|
+
round2: () => round2$3
|
|
2603
|
+
});
|
|
2604
|
+
|
|
2605
|
+
//#endregion
|
|
2606
|
+
//#region src/cmd/dkg/index.ts
|
|
2607
|
+
var dkg_exports = /* @__PURE__ */ __exportAll({
|
|
2608
|
+
buildGroupParticipants: () => buildGroupParticipants,
|
|
2609
|
+
coordinator: () => coordinator_exports$1,
|
|
2610
|
+
dkgStateDir: () => dkgStateDir,
|
|
2611
|
+
formatNameWithOwnerMarker: () => formatNameWithOwnerMarker,
|
|
2612
|
+
groupParticipantFromRegistry: () => groupParticipantFromRegistry,
|
|
2613
|
+
groupStateDir: () => groupStateDir,
|
|
2614
|
+
parseAridUr: () => parseAridUr,
|
|
2615
|
+
parseEnvelopeUr: () => parseEnvelopeUr,
|
|
2616
|
+
participant: () => participant_exports$1,
|
|
2617
|
+
participantNamesFromRegistry: () => participantNamesFromRegistry,
|
|
2618
|
+
resolveParticipants: () => resolveParticipants,
|
|
2619
|
+
resolveSender: () => resolveSender,
|
|
2620
|
+
resolveSenderName: () => resolveSenderName$1,
|
|
2621
|
+
signingKeyFromVerifying: () => signingKeyFromVerifying
|
|
2622
|
+
});
|
|
2623
|
+
|
|
2624
|
+
//#endregion
|
|
2625
|
+
//#region src/cmd/sign/common.ts
|
|
2626
|
+
/**
|
|
2627
|
+
* Common utilities for sign commands.
|
|
2628
|
+
*
|
|
2629
|
+
* Port of cmd/sign/common.rs from frost-hubert-rust.
|
|
2630
|
+
*
|
|
2631
|
+
* @module
|
|
2632
|
+
*/
|
|
2633
|
+
/**
|
|
2634
|
+
* Get the signing state directory for a group (without session).
|
|
2635
|
+
*
|
|
2636
|
+
* Path: `{registry_dir}/group-state/{group_id.hex()}/signing`
|
|
2637
|
+
*
|
|
2638
|
+
* Port of `signing_state_dir_for_group()` from cmd/sign/common.rs.
|
|
2639
|
+
*/
|
|
2640
|
+
function signingStateDirForGroup(registryPath, groupIdHex) {
|
|
2641
|
+
return `${groupStateDir(registryPath, groupIdHex)}/signing`;
|
|
2642
|
+
}
|
|
2643
|
+
/**
|
|
2644
|
+
* Get the signing state directory for a given registry path, group ID, and session ID.
|
|
2645
|
+
*
|
|
2646
|
+
* Path: `{registry_dir}/group-state/{group_id.hex()}/signing/{session_id.hex()}`
|
|
2647
|
+
*
|
|
2648
|
+
* Port of `signing_state_dir()` from cmd/sign/common.rs.
|
|
2649
|
+
*/
|
|
2650
|
+
function signingStateDir(registryPath, groupIdHex, sessionIdHex) {
|
|
2651
|
+
return `${signingStateDirForGroup(registryPath, groupIdHex)}/${sessionIdHex}`;
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* Content wrapper for signFinalize events.
|
|
2655
|
+
*
|
|
2656
|
+
* This wraps an envelope with a unit subject and type assertion
|
|
2657
|
+
* "signFinalize", implementing the traits required by SealedEvent<T>.
|
|
2658
|
+
*
|
|
2659
|
+
* Port of `struct SignFinalizeContent` from cmd/sign/common.rs.
|
|
2660
|
+
*/
|
|
2661
|
+
var SignFinalizeContent = class SignFinalizeContent {
|
|
2662
|
+
_envelope;
|
|
2663
|
+
constructor(envelope) {
|
|
2664
|
+
this._envelope = envelope;
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* Creates a new SignFinalizeContent with a unit subject and type assertion.
|
|
2668
|
+
*
|
|
2669
|
+
* Port of `SignFinalizeContent::new()` from cmd/sign/common.rs.
|
|
2670
|
+
*/
|
|
2671
|
+
static new() {
|
|
2672
|
+
return new SignFinalizeContent(Envelope.unit().addType("signFinalize"));
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2675
|
+
* Adds an assertion to the content envelope.
|
|
2676
|
+
*
|
|
2677
|
+
* Port of `SignFinalizeContent::add_assertion()` from cmd/sign/common.rs.
|
|
2678
|
+
*/
|
|
2679
|
+
addAssertion(predicate, object) {
|
|
2680
|
+
return new SignFinalizeContent(this._envelope.addAssertion(predicate, object));
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Returns the inner envelope.
|
|
2684
|
+
*
|
|
2685
|
+
* Port of `SignFinalizeContent::envelope()` from cmd/sign/common.rs.
|
|
2686
|
+
*/
|
|
2687
|
+
envelope() {
|
|
2688
|
+
return this._envelope;
|
|
2689
|
+
}
|
|
2690
|
+
/**
|
|
2691
|
+
* Creates a SignFinalizeContent from an envelope with validation.
|
|
2692
|
+
*
|
|
2693
|
+
* Validates that the envelope has a unit subject and type "signFinalize".
|
|
2694
|
+
*
|
|
2695
|
+
* Port of `TryFrom<Envelope> for SignFinalizeContent` from cmd/sign/common.rs.
|
|
2696
|
+
*/
|
|
2697
|
+
static fromEnvelope(envelope) {
|
|
2698
|
+
envelope.checkSubjectUnit();
|
|
2699
|
+
envelope.checkType("signFinalize");
|
|
2700
|
+
return new SignFinalizeContent(envelope);
|
|
2701
|
+
}
|
|
2702
|
+
/**
|
|
2703
|
+
* Converts this SignFinalizeContent to an Envelope.
|
|
2704
|
+
*
|
|
2705
|
+
* Port of `From<SignFinalizeContent> for Envelope` from cmd/sign/common.rs.
|
|
2706
|
+
*/
|
|
2707
|
+
toEnvelope() {
|
|
2708
|
+
return this._envelope;
|
|
2709
|
+
}
|
|
2710
|
+
};
|
|
2711
|
+
|
|
2712
|
+
//#endregion
|
|
2713
|
+
//#region src/cmd/sign/coordinator/invite.ts
|
|
2714
|
+
/**
|
|
2715
|
+
* Sign coordinator invite command.
|
|
2716
|
+
*
|
|
2717
|
+
* Port of cmd/sign/coordinator/invite.rs from frost-hubert-rust.
|
|
2718
|
+
*
|
|
2719
|
+
* @module
|
|
2720
|
+
*/
|
|
2721
|
+
/**
|
|
2722
|
+
* Create new session ARIDs for all participants.
|
|
2723
|
+
*
|
|
2724
|
+
* Port of `SessionArids::new()` from cmd/sign/coordinator/invite.rs lines 158-173.
|
|
2725
|
+
*/
|
|
2726
|
+
function createSessionArids(participants) {
|
|
2727
|
+
const commitArids = /* @__PURE__ */ new Map();
|
|
2728
|
+
const shareArids = /* @__PURE__ */ new Map();
|
|
2729
|
+
for (const participant of participants) {
|
|
2730
|
+
const xidKey = participant.xid().urString();
|
|
2731
|
+
commitArids.set(xidKey, ARID.new());
|
|
2732
|
+
shareArids.set(xidKey, ARID.new());
|
|
2733
|
+
}
|
|
2734
|
+
return {
|
|
2735
|
+
sessionId: ARID.new(),
|
|
2736
|
+
startArid: ARID.new(),
|
|
2737
|
+
commitArids,
|
|
2738
|
+
shareArids
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Validate that the owner is the coordinator of the group.
|
|
2743
|
+
*
|
|
2744
|
+
* Port of `validate_coordinator()` from cmd/sign/coordinator/invite.rs lines 179-192.
|
|
2745
|
+
*/
|
|
2746
|
+
function validateCoordinator(groupRecord, owner) {
|
|
2747
|
+
if (groupRecord.coordinator().xid().urString() !== owner.xid().urString()) throw new Error(`Only the coordinator can start signing. Coordinator: ${groupRecord.coordinator().xid().urString()}, Owner: ${owner.xid().urString()}`);
|
|
2748
|
+
}
|
|
2749
|
+
/**
|
|
2750
|
+
* Gather XIDDocuments for all participants from the registry.
|
|
2751
|
+
*
|
|
2752
|
+
* Port of `gather_recipient_documents()` from cmd/sign/coordinator/invite.rs lines 198-222.
|
|
2753
|
+
*/
|
|
2754
|
+
function gatherRecipientDocuments(participants, owner, registry) {
|
|
2755
|
+
const recipientDocs = [];
|
|
2756
|
+
for (const participant of participants) {
|
|
2757
|
+
const xid = participant.xid();
|
|
2758
|
+
if (xid.urString() === owner.xid().urString()) recipientDocs.push(owner.xidDocument());
|
|
2759
|
+
else {
|
|
2760
|
+
const record = registry.participant(xid);
|
|
2761
|
+
if (record === void 0) throw new Error(`Participant ${xid.urString()} not found in registry`);
|
|
2762
|
+
recipientDocs.push(record.xidDocument());
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
return recipientDocs;
|
|
2766
|
+
}
|
|
2767
|
+
/**
|
|
2768
|
+
* Build the sign invite request.
|
|
2769
|
+
*
|
|
2770
|
+
* Port of `build_sign_invite_request()` from cmd/sign/coordinator/invite.rs lines 239-284.
|
|
2771
|
+
*/
|
|
2772
|
+
function buildSignInviteRequest(ctx) {
|
|
2773
|
+
let request = SealedRequest.new("signInvite", ctx.arids.sessionId, ctx.owner.xidDocument()).withParameter("group", ctx.groupId).withParameter("session", ctx.arids.sessionId).withParameter("target", ctx.targetEnvelope).withParameter("minSigners", ctx.groupRecord.minSigners()).withDate(/* @__PURE__ */ new Date()).withParameter("validUntil", CborDate.fromDatetime(ctx.validUntil));
|
|
2774
|
+
for (const participant of ctx.participants) {
|
|
2775
|
+
const xid = participant.xid();
|
|
2776
|
+
const xidKey = xid.urString();
|
|
2777
|
+
let participantDoc;
|
|
2778
|
+
if (xidKey === ctx.owner.xid().urString()) participantDoc = ctx.owner.xidDocument();
|
|
2779
|
+
else {
|
|
2780
|
+
const record = ctx.registry.participant(xid);
|
|
2781
|
+
if (record === void 0) throw new Error("Participant not found in registry");
|
|
2782
|
+
participantDoc = record.xidDocument();
|
|
2783
|
+
}
|
|
2784
|
+
const encryptionKey = participantDoc.encryptionKey();
|
|
2785
|
+
if (encryptionKey === void 0) throw new Error("Participant XID document has no encryption key");
|
|
2786
|
+
const responseArid = ctx.arids.commitArids.get(xidKey);
|
|
2787
|
+
if (responseArid === void 0) throw new Error("commit ARID not found for participant");
|
|
2788
|
+
const encryptedResponseArid = responseArid.toEnvelope().encryptToRecipient(encryptionKey);
|
|
2789
|
+
const participantEntry = Envelope.new(xid).addAssertion("response_arid", encryptedResponseArid);
|
|
2790
|
+
request = request.withParameter("participant", participantEntry);
|
|
2791
|
+
}
|
|
2792
|
+
return request;
|
|
2793
|
+
}
|
|
2794
|
+
/**
|
|
2795
|
+
* Build the session state JSON for persistence.
|
|
2796
|
+
*
|
|
2797
|
+
* Port of `build_session_state_json()` from cmd/sign/coordinator/invite.rs lines 290-346.
|
|
2798
|
+
*/
|
|
2799
|
+
function buildSessionStateJson(arids, groupId, groupRecord, participants, targetEnvelope) {
|
|
2800
|
+
const participantsMap = {};
|
|
2801
|
+
for (const participant of participants) {
|
|
2802
|
+
const xidKey = participant.xid().urString();
|
|
2803
|
+
const commitArid = arids.commitArids.get(xidKey);
|
|
2804
|
+
const shareArid = arids.shareArids.get(xidKey);
|
|
2805
|
+
if (commitArid === void 0 || shareArid === void 0) throw new Error("ARID not found for participant");
|
|
2806
|
+
participantsMap[xidKey] = {
|
|
2807
|
+
commit_arid: commitArid.urString(),
|
|
2808
|
+
share_arid: shareArid.urString()
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
return {
|
|
2812
|
+
session_id: arids.sessionId.urString(),
|
|
2813
|
+
start_arid: arids.startArid.urString(),
|
|
2814
|
+
group: groupId.urString(),
|
|
2815
|
+
min_signers: groupRecord.minSigners(),
|
|
2816
|
+
participants: participantsMap,
|
|
2817
|
+
target: targetEnvelope.urString()
|
|
2818
|
+
};
|
|
2819
|
+
}
|
|
2820
|
+
/**
|
|
2821
|
+
* Persist the session state to disk.
|
|
2822
|
+
*
|
|
2823
|
+
* Port of `persist_session_state()` from cmd/sign/coordinator/invite.rs lines 348-356.
|
|
2824
|
+
*/
|
|
2825
|
+
function persistSessionState(signingDir, stateJson) {
|
|
2826
|
+
fs.mkdirSync(signingDir, { recursive: true });
|
|
2827
|
+
const startStatePath = path.join(signingDir, "start.json");
|
|
2828
|
+
fs.writeFileSync(startStatePath, JSON.stringify(stateJson, null, 2));
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Load an envelope from a file path.
|
|
2832
|
+
*
|
|
2833
|
+
* Port of `load_envelope_from_path()` from cmd/sign/coordinator/invite.rs lines 385-392.
|
|
2834
|
+
*/
|
|
2835
|
+
function loadEnvelopeFromPath(filePath) {
|
|
2836
|
+
if (!fs.existsSync(filePath)) throw new Error(`Failed to read target envelope from ${filePath}`);
|
|
2837
|
+
const trimmed = fs.readFileSync(filePath, "utf-8").trim();
|
|
2838
|
+
try {
|
|
2839
|
+
return Envelope.fromURString(trimmed);
|
|
2840
|
+
} catch (e) {
|
|
2841
|
+
throw new Error(`Failed to load target envelope from ${filePath}: ${String(e)}`);
|
|
2842
|
+
}
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Execute the sign coordinator invite command.
|
|
2846
|
+
*
|
|
2847
|
+
* Invites participants to sign a target envelope.
|
|
2848
|
+
*
|
|
2849
|
+
* Port of `CommandArgs::exec()` from cmd/sign/coordinator/invite.rs lines 44-144.
|
|
2850
|
+
*/
|
|
2851
|
+
async function invite(client, options, cwd) {
|
|
2852
|
+
if (client !== void 0 && options.preview === true) throw new Error("--preview cannot be used with Hubert storage options");
|
|
2853
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
2854
|
+
const registry = Registry.load(registryPath);
|
|
2855
|
+
const owner = registry.owner();
|
|
2856
|
+
if (owner === void 0) throw new Error("Registry owner is required");
|
|
2857
|
+
const groupId = parseAridUr(options.groupId);
|
|
2858
|
+
const groupRecord = registry.group(groupId);
|
|
2859
|
+
if (groupRecord === void 0) throw new Error(`Group ${options.groupId} not found in registry`);
|
|
2860
|
+
validateCoordinator(groupRecord, owner);
|
|
2861
|
+
const targetEnvelope = loadEnvelopeFromPath(path.resolve(cwd, options.targetFile));
|
|
2862
|
+
const participants = groupRecord.participants();
|
|
2863
|
+
const recipientDocs = gatherRecipientDocuments(participants, owner, registry);
|
|
2864
|
+
const signerKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
2865
|
+
if (signerKeys === void 0) throw new Error("Coordinator XID document has no signing keys");
|
|
2866
|
+
const sessionArids = createSessionArids(participants);
|
|
2867
|
+
const validDays = options.validDays ?? 1 / 24;
|
|
2868
|
+
const validUntil = new Date(Date.now() + validDays * 24 * 60 * 60 * 1e3);
|
|
2869
|
+
const request = buildSignInviteRequest({
|
|
2870
|
+
arids: sessionArids,
|
|
2871
|
+
groupId,
|
|
2872
|
+
targetEnvelope,
|
|
2873
|
+
groupRecord,
|
|
2874
|
+
owner,
|
|
2875
|
+
registry,
|
|
2876
|
+
participants,
|
|
2877
|
+
validUntil
|
|
2878
|
+
});
|
|
2879
|
+
const stateJson = buildSessionStateJson(sessionArids, groupId, groupRecord, participants, targetEnvelope);
|
|
2880
|
+
const recipientRefs = recipientDocs;
|
|
2881
|
+
const sealedEnvelope = request.toEnvelopeForRecipients(validUntil, signerKeys, recipientRefs);
|
|
2882
|
+
if (options.preview === true) {
|
|
2883
|
+
const unsealed = request.toEnvelope(void 0, signerKeys, void 0);
|
|
2884
|
+
console.log(unsealed.urString());
|
|
2885
|
+
return {
|
|
2886
|
+
sessionId: sessionArids.sessionId.urString(),
|
|
2887
|
+
startArid: sessionArids.startArid.urString()
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
persistSessionState(signingStateDir(registryPath, groupId.hex(), sessionArids.sessionId.hex()), stateJson);
|
|
2891
|
+
if (client === void 0) throw new Error("Hubert storage is required for sign start");
|
|
2892
|
+
await putWithIndicator(client, sessionArids.startArid, sealedEnvelope, "Signing invite", options.verbose ?? false);
|
|
2893
|
+
if (options.verbose === true) {
|
|
2894
|
+
console.log(`Session ID: ${sessionArids.sessionId.urString()}`);
|
|
2895
|
+
console.log(`Start ARID: ${sessionArids.startArid.urString()}`);
|
|
2896
|
+
}
|
|
2897
|
+
console.log(sessionArids.startArid.urString());
|
|
2898
|
+
return {
|
|
2899
|
+
sessionId: sessionArids.sessionId.urString(),
|
|
2900
|
+
startArid: sessionArids.startArid.urString()
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
//#endregion
|
|
2905
|
+
//#region src/cmd/sign/coordinator/round1.ts
|
|
2906
|
+
/**
|
|
2907
|
+
* Sign coordinator round 1 command.
|
|
2908
|
+
*
|
|
2909
|
+
* Port of cmd/sign/coordinator/round1.rs from frost-hubert-rust.
|
|
2910
|
+
*
|
|
2911
|
+
* @module
|
|
2912
|
+
*/
|
|
2913
|
+
/**
|
|
2914
|
+
* Load the start state for a signing session.
|
|
2915
|
+
*
|
|
2916
|
+
* Port of `load_start_state()` from cmd/sign/coordinator/round1.rs.
|
|
2917
|
+
*/
|
|
2918
|
+
function loadStartState$1(registryPath, sessionId, groupHint) {
|
|
2919
|
+
const base = path.dirname(registryPath);
|
|
2920
|
+
const groupStateDir = path.join(base, "group-state");
|
|
2921
|
+
const candidatePaths = [];
|
|
2922
|
+
let groupDirs;
|
|
2923
|
+
if (groupHint !== void 0) groupDirs = [[groupHint, path.join(groupStateDir, groupHint.hex())]];
|
|
2924
|
+
else {
|
|
2925
|
+
groupDirs = [];
|
|
2926
|
+
if (fs.existsSync(groupStateDir)) {
|
|
2927
|
+
for (const entry of fs.readdirSync(groupStateDir, { withFileTypes: true })) if (entry.isDirectory()) {
|
|
2928
|
+
const dirName = entry.name;
|
|
2929
|
+
if (dirName.length === 64 && /^[0-9a-fA-F]+$/.test(dirName)) {
|
|
2930
|
+
const groupId = ARID.fromHex(dirName);
|
|
2931
|
+
groupDirs.push([groupId, path.join(groupStateDir, dirName)]);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
for (const [groupId, groupDir] of groupDirs) {
|
|
2937
|
+
const candidate = path.join(groupDir, "signing", sessionId.hex(), "start.json");
|
|
2938
|
+
if (fs.existsSync(candidate)) candidatePaths.push([groupId, candidate]);
|
|
2939
|
+
}
|
|
2940
|
+
if (candidatePaths.length === 0) throw new Error("No sign start state found; run `frost sign coordinator start` first");
|
|
2941
|
+
if (candidatePaths.length > 1) throw new Error("Multiple signing sessions found; specify --group to disambiguate");
|
|
2942
|
+
const [groupId, statePath] = candidatePaths[0];
|
|
2943
|
+
const raw = JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
2944
|
+
const getStr = (key) => {
|
|
2945
|
+
const value = raw[key];
|
|
2946
|
+
if (typeof value !== "string") throw new Error(`Missing or invalid ${key} in start.json`);
|
|
2947
|
+
return value;
|
|
2948
|
+
};
|
|
2949
|
+
const sessionInState = parseAridUr(getStr("session_id"));
|
|
2950
|
+
const groupInState = parseAridUr(getStr("group"));
|
|
2951
|
+
if (sessionInState.hex() !== sessionId.hex()) throw new Error(`start.json session ${sessionInState.urString()} does not match requested session ${sessionId.urString()}`);
|
|
2952
|
+
if (groupInState.hex() !== groupId.hex()) throw new Error(`start.json group ${groupInState.urString()} does not match directory group ${groupId.urString()}`);
|
|
2953
|
+
const targetUr = getStr("target");
|
|
2954
|
+
const participantsVal = raw["participants"];
|
|
2955
|
+
if (participantsVal === void 0 || typeof participantsVal !== "object") throw new Error("Missing participants in start.json");
|
|
2956
|
+
const participants = /* @__PURE__ */ new Map();
|
|
2957
|
+
for (const [xidStr, value] of Object.entries(participantsVal)) {
|
|
2958
|
+
const xid = XID.fromURString(xidStr);
|
|
2959
|
+
if (typeof value !== "object" || value === null) throw new Error("Participant entry is not an object in start.json");
|
|
2960
|
+
const commitAridStr = value["commit_arid"];
|
|
2961
|
+
const shareAridStr = value["share_arid"];
|
|
2962
|
+
if (typeof commitAridStr !== "string") throw new Error("Missing commit_arid in start.json");
|
|
2963
|
+
if (typeof shareAridStr !== "string") throw new Error("Missing share_arid in start.json");
|
|
2964
|
+
participants.set(xid.urString(), {
|
|
2965
|
+
commitArid: parseAridUr(commitAridStr),
|
|
2966
|
+
shareArid: parseAridUr(shareAridStr)
|
|
2967
|
+
});
|
|
2968
|
+
}
|
|
2969
|
+
return {
|
|
2970
|
+
groupId,
|
|
2971
|
+
targetUr,
|
|
2972
|
+
participants
|
|
2973
|
+
};
|
|
2974
|
+
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Validate and extract data from a sign commit response.
|
|
2977
|
+
*
|
|
2978
|
+
* Port of `validate_and_extract_sign_round1_response()` from cmd/sign/coordinator/round1.rs.
|
|
2979
|
+
*/
|
|
2980
|
+
function validateAndExtractCommitResponse(envelope, coordinatorKeys, expectedSender, expectedSessionId) {
|
|
2981
|
+
const now = /* @__PURE__ */ new Date();
|
|
2982
|
+
const sealedResponse = SealedResponse.tryFromEncryptedEnvelope(envelope, void 0, now, coordinatorKeys);
|
|
2983
|
+
if (!sealedResponse.sender().xid().equals(expectedSender)) throw new Error(`Unexpected response sender: ${sealedResponse.sender().xid().urString()} (expected ${expectedSender.urString()})`);
|
|
2984
|
+
if (sealedResponse.isErr()) {
|
|
2985
|
+
const errorEnvelope = sealedResponse.error();
|
|
2986
|
+
let reason = "unknown reason";
|
|
2987
|
+
try {
|
|
2988
|
+
const reasonEnv = errorEnvelope.objectForPredicate("reason");
|
|
2989
|
+
if (reasonEnv !== void 0) reason = reasonEnv.extractString();
|
|
2990
|
+
} catch {}
|
|
2991
|
+
throw new Error(`Participant rejected signInvite: ${reason}`);
|
|
2992
|
+
}
|
|
2993
|
+
const result = sealedResponse.result();
|
|
2994
|
+
result.checkSubjectUnit();
|
|
2995
|
+
result.checkType("signRound1Response");
|
|
2996
|
+
const responseSession = result.tryObjectForPredicate("session", (cbor) => ARID.fromTaggedCbor(cbor));
|
|
2997
|
+
if (responseSession.hex() !== expectedSessionId.hex()) throw new Error(`Response session ${responseSession.urString()} does not match expected ${expectedSessionId.urString()}`);
|
|
2998
|
+
const commitmentsJson = result.tryObjectForPredicate("commitments", (cbor) => JSON$1.fromTaggedCbor(cbor));
|
|
2999
|
+
return {
|
|
3000
|
+
commitments: JSON.parse(new TextDecoder().decode(commitmentsJson.toData())),
|
|
3001
|
+
nextRequestArid: result.tryObjectForPredicate("response_arid", (cbor) => ARID.fromTaggedCbor(cbor))
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
/**
|
|
3005
|
+
* Collect signing commitments in parallel.
|
|
3006
|
+
*
|
|
3007
|
+
* Port of `collect_sign_round1_parallel()` from cmd/sign/coordinator/round1.rs.
|
|
3008
|
+
*/
|
|
3009
|
+
async function collectCommitmentsParallel(client, registry, startState, coordinator, sessionId, timeout) {
|
|
3010
|
+
const requests = [];
|
|
3011
|
+
for (const [xidStr, state] of startState.participants) {
|
|
3012
|
+
const xid = XID.fromURString(xidStr);
|
|
3013
|
+
const name = registry.participant(xid)?.petName() ?? xid.urString();
|
|
3014
|
+
requests.push([
|
|
3015
|
+
xid,
|
|
3016
|
+
state.commitArid,
|
|
3017
|
+
name
|
|
3018
|
+
]);
|
|
3019
|
+
}
|
|
3020
|
+
const coordinatorKeys = coordinator.inceptionPrivateKeys();
|
|
3021
|
+
if (coordinatorKeys === void 0) throw new Error("Missing coordinator private keys");
|
|
3022
|
+
return parallelFetch(client, requests, (envelope, xid) => {
|
|
3023
|
+
try {
|
|
3024
|
+
return validateAndExtractCommitResponse(envelope, coordinatorKeys, xid, sessionId);
|
|
3025
|
+
} catch (e) {
|
|
3026
|
+
return { rejected: e instanceof Error ? e.message : String(e) };
|
|
3027
|
+
}
|
|
3028
|
+
}, { timeoutSeconds: timeout });
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Build a sign share request for a participant.
|
|
3032
|
+
*
|
|
3033
|
+
* Port of `build_sign_share_request()` from cmd/sign/coordinator/round1.rs.
|
|
3034
|
+
*/
|
|
3035
|
+
function buildShareRequestForParticipant(sender, _groupId, sessionId, responseArid, commitments) {
|
|
3036
|
+
let request = SealedRequest.new("signRound2", sessionId, sender).withParameter("session", sessionId).withParameter("response_arid", responseArid);
|
|
3037
|
+
for (const [xidStr, commits] of commitments) {
|
|
3038
|
+
const xid = XID.fromURString(xidStr);
|
|
3039
|
+
const commitsJson = JSON$1.fromData(new TextEncoder().encode(JSON.stringify(commits)));
|
|
3040
|
+
const entry = Envelope.new(xid).addAssertion("commitments", commitsJson.taggedCborData());
|
|
3041
|
+
request = request.withParameter("commitment", entry);
|
|
3042
|
+
}
|
|
3043
|
+
return request;
|
|
3044
|
+
}
|
|
3045
|
+
/**
|
|
3046
|
+
* Dispatch share requests to participants in parallel.
|
|
3047
|
+
*
|
|
3048
|
+
* Port of parallel dispatch logic from cmd/sign/coordinator/round1.rs.
|
|
3049
|
+
*/
|
|
3050
|
+
async function dispatchShareRequestsParallel(client, registry, owner, startState, sessionId, collection, commitments, previewShare, verbose) {
|
|
3051
|
+
const signerKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
3052
|
+
if (signerKeys === void 0) throw new Error("Coordinator XID document has no signing keys");
|
|
3053
|
+
const validUntil = new Date(Date.now() + 3600 * 1e3);
|
|
3054
|
+
const messages = [];
|
|
3055
|
+
let previewPrinted = false;
|
|
3056
|
+
for (const [xid, data] of collection.successes) {
|
|
3057
|
+
const xidStr = xid.urString();
|
|
3058
|
+
const participantState = startState.participants.get(xidStr);
|
|
3059
|
+
if (participantState === void 0) throw new Error(`Participant ${xidStr} not found in start state`);
|
|
3060
|
+
const participantName = registry.participant(xid)?.petName() ?? xidStr;
|
|
3061
|
+
let recipientDoc;
|
|
3062
|
+
if (xid.urString() === owner.xid().urString()) recipientDoc = owner.xidDocument();
|
|
3063
|
+
else {
|
|
3064
|
+
const record = registry.participant(xid);
|
|
3065
|
+
if (record === void 0) throw new Error(`Participant ${xidStr} not found in registry`);
|
|
3066
|
+
recipientDoc = record.xidDocument();
|
|
3067
|
+
}
|
|
3068
|
+
const request = buildShareRequestForParticipant(owner.xidDocument(), startState.groupId, sessionId, participantState.shareArid, commitments);
|
|
3069
|
+
if (previewShare === true && !previewPrinted) {
|
|
3070
|
+
const preview = request.toEnvelope(validUntil, signerKeys, void 0);
|
|
3071
|
+
console.log(`# signRound2 preview for ${xidStr}`);
|
|
3072
|
+
console.log(preview.format());
|
|
3073
|
+
previewPrinted = true;
|
|
3074
|
+
}
|
|
3075
|
+
const sealedEnvelope = request.toEnvelopeForRecipients(validUntil, signerKeys, [recipientDoc]);
|
|
3076
|
+
messages.push([
|
|
3077
|
+
xid,
|
|
3078
|
+
data.nextRequestArid,
|
|
3079
|
+
sealedEnvelope,
|
|
3080
|
+
participantName
|
|
3081
|
+
]);
|
|
3082
|
+
}
|
|
3083
|
+
console.error();
|
|
3084
|
+
return parallelSend(client, messages, verbose);
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Persist collected commitments to disk.
|
|
3088
|
+
*
|
|
3089
|
+
* Port of commitments persistence logic from cmd/sign/coordinator/round1.rs.
|
|
3090
|
+
*/
|
|
3091
|
+
function persistCommitments(registryPath, groupId, sessionId, startState, commitments) {
|
|
3092
|
+
const signingDir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
|
|
3093
|
+
fs.mkdirSync(signingDir, { recursive: true });
|
|
3094
|
+
const commitmentsPath = path.join(signingDir, "commitments.json");
|
|
3095
|
+
const commitmentsJson = {};
|
|
3096
|
+
for (const [xidStr, commits] of commitments) {
|
|
3097
|
+
const participantState = startState.participants.get(xidStr);
|
|
3098
|
+
if (participantState === void 0) throw new Error(`Participant ${xidStr} not found in start state`);
|
|
3099
|
+
commitmentsJson[xidStr] = {
|
|
3100
|
+
commitments: commits,
|
|
3101
|
+
share_arid: participantState.shareArid.urString()
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
const root = {
|
|
3105
|
+
group: groupId.urString(),
|
|
3106
|
+
session: sessionId.urString(),
|
|
3107
|
+
target: startState.targetUr,
|
|
3108
|
+
commitments: commitmentsJson
|
|
3109
|
+
};
|
|
3110
|
+
fs.writeFileSync(commitmentsPath, JSON.stringify(root, null, 2));
|
|
3111
|
+
return commitmentsPath;
|
|
3112
|
+
}
|
|
3113
|
+
/**
|
|
3114
|
+
* Update pending requests in the registry for share phase.
|
|
3115
|
+
*
|
|
3116
|
+
* Note: In the Rust implementation, the registry doesn't track pending ARIDs
|
|
3117
|
+
* directly on participant records. The pending state is managed through the
|
|
3118
|
+
* start.json and commitments.json files in the signing state directory.
|
|
3119
|
+
*
|
|
3120
|
+
* This function is provided for API compatibility but currently does nothing.
|
|
3121
|
+
*/
|
|
3122
|
+
function updatePendingForShare(_registry, _collection, _startState) {}
|
|
3123
|
+
/**
|
|
3124
|
+
* Execute the sign coordinator round 1 command.
|
|
3125
|
+
*
|
|
3126
|
+
* Collects signing commitments from participants.
|
|
3127
|
+
*
|
|
3128
|
+
* Port of `round1()` from cmd/sign/coordinator/round1.rs.
|
|
3129
|
+
*/
|
|
3130
|
+
async function round1$2(client, options, cwd) {
|
|
3131
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
3132
|
+
const registry = Registry.load(registryPath);
|
|
3133
|
+
const owner = registry.owner();
|
|
3134
|
+
if (owner === void 0) throw new Error("Registry owner is required");
|
|
3135
|
+
const sessionId = parseAridUr(options.sessionId);
|
|
3136
|
+
const startState = loadStartState$1(registryPath, sessionId, options.groupId !== void 0 ? parseAridUr(options.groupId) : void 0);
|
|
3137
|
+
const groupId = startState.groupId;
|
|
3138
|
+
const groupRecord = registry.group(groupId);
|
|
3139
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
3140
|
+
if (groupRecord.coordinator().xid().urString() !== owner.xid().urString()) throw new Error(`Only the coordinator can collect signInvite responses. Coordinator: ${groupRecord.coordinator().xid().urString()}, Owner: ${owner.xid().urString()}`);
|
|
3141
|
+
if (options.parallel === true) {
|
|
3142
|
+
const collection = await collectCommitmentsParallel(client, registry, startState, owner.xidDocument(), sessionId, options.timeoutSeconds);
|
|
3143
|
+
if (collection.rejections.length > 0) {
|
|
3144
|
+
console.error();
|
|
3145
|
+
console.error("Rejections:");
|
|
3146
|
+
for (const [xid, reason] of collection.rejections) console.error(` ${xid.urString()}: ${reason}`);
|
|
3147
|
+
}
|
|
3148
|
+
if (collection.errors.length > 0) {
|
|
3149
|
+
console.error();
|
|
3150
|
+
console.error("Errors:");
|
|
3151
|
+
for (const [xid, error] of collection.errors) console.error(` ${xid.urString()}: ${error}`);
|
|
3152
|
+
}
|
|
3153
|
+
if (collection.timeouts.length > 0) {
|
|
3154
|
+
console.error();
|
|
3155
|
+
console.error("Timeouts:");
|
|
3156
|
+
for (const xid of collection.timeouts) console.error(` ${xid.urString()}`);
|
|
3157
|
+
}
|
|
3158
|
+
if (!collection.allSucceeded()) throw new Error(`Sign commit collection incomplete: ${collection.successes.length} succeeded, ${collection.rejections.length} rejected, ${collection.errors.length} errors, ${collection.timeouts.length} timeouts`);
|
|
3159
|
+
const commitments = /* @__PURE__ */ new Map();
|
|
3160
|
+
for (const [xid, data] of collection.successes) commitments.set(xid.urString(), data.commitments);
|
|
3161
|
+
const commitmentsPath = persistCommitments(registryPath, groupId, sessionId, startState, commitments);
|
|
3162
|
+
const failures = (await dispatchShareRequestsParallel(client, registry, owner, startState, sessionId, collection, commitments, options.previewShare, options.verbose)).filter(([_, err]) => err !== null);
|
|
3163
|
+
if (failures.length > 0) {
|
|
3164
|
+
for (const [xid, error] of failures) if (error !== null) console.error(`Failed to send to ${xid.urString()}: ${error.message}`);
|
|
3165
|
+
throw new Error(`Failed to send signRound2 requests to ${failures.length} participants`);
|
|
3166
|
+
}
|
|
3167
|
+
/* @__PURE__ */ updatePendingForShare(registry, collection, startState);
|
|
3168
|
+
const displayPath = path.relative(cwd, commitmentsPath) || commitmentsPath;
|
|
3169
|
+
if (isVerbose() || options.verbose === true) {
|
|
3170
|
+
console.error();
|
|
3171
|
+
console.error(`Collected ${collection.successes.length} signInvite responses. Saved to ${displayPath}`);
|
|
3172
|
+
console.error(`Dispatched ${collection.successes.length} signRound2 requests.`);
|
|
3173
|
+
}
|
|
3174
|
+
return {
|
|
3175
|
+
accepted: collection.successes.length,
|
|
3176
|
+
rejected: collection.rejections.length,
|
|
3177
|
+
errors: collection.errors.length,
|
|
3178
|
+
timeouts: collection.timeouts.length
|
|
3179
|
+
};
|
|
3180
|
+
} else {
|
|
3181
|
+
if (isVerbose() || options.verbose === true) console.error(`Collecting signInvite responses for session ${sessionId.urString()} from ${startState.participants.size} participants...`);
|
|
3182
|
+
const commitments = /* @__PURE__ */ new Map();
|
|
3183
|
+
const sendToArids = /* @__PURE__ */ new Map();
|
|
3184
|
+
const errors = [];
|
|
3185
|
+
const coordinatorKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
3186
|
+
if (coordinatorKeys === void 0) throw new Error("Coordinator XID document has no inception private keys");
|
|
3187
|
+
for (const [xidStr, participantState] of startState.participants) {
|
|
3188
|
+
const xid = XID.fromURString(xidStr);
|
|
3189
|
+
const participantName = registry.participant(xid)?.petName() ?? xidStr;
|
|
3190
|
+
try {
|
|
3191
|
+
const envelope = await client.get(participantState.commitArid, options.timeoutSeconds);
|
|
3192
|
+
if (envelope === void 0) throw new Error("Response not found in Hubert storage");
|
|
3193
|
+
const data = validateAndExtractCommitResponse(envelope, coordinatorKeys, xid, sessionId);
|
|
3194
|
+
commitments.set(xidStr, data.commitments);
|
|
3195
|
+
sendToArids.set(xidStr, data.nextRequestArid);
|
|
3196
|
+
if (isVerbose() || options.verbose === true) console.error(` ✓ ${participantName}`);
|
|
3197
|
+
} catch (e) {
|
|
3198
|
+
errors.push([xid, e instanceof Error ? e.message : String(e)]);
|
|
3199
|
+
if (isVerbose() || options.verbose === true) console.error(` ✗ ${participantName}: ${e instanceof Error ? e.message : String(e)}`);
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
if (errors.length > 0) throw new Error(`Sign commit collection incomplete: ${errors.length} of ${startState.participants.size} responses failed`);
|
|
3203
|
+
if (commitments.size !== startState.participants.size) {
|
|
3204
|
+
const missing = [];
|
|
3205
|
+
for (const xidStr of startState.participants.keys()) if (!commitments.has(xidStr)) missing.push(xidStr);
|
|
3206
|
+
throw new Error(`Missing signInvite responses from: ${missing.join(", ")}`);
|
|
3207
|
+
}
|
|
3208
|
+
const commitmentsPath = persistCommitments(registryPath, groupId, sessionId, startState, commitments);
|
|
3209
|
+
const signerKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
3210
|
+
if (signerKeys === void 0) throw new Error("Owner XID document has no inception private keys");
|
|
3211
|
+
const validUntil = new Date(Date.now() + 3600 * 1e3);
|
|
3212
|
+
if (isVerbose() || options.verbose === true) console.error(`Dispatching signRound2 requests to ${sendToArids.size} participants...`);
|
|
3213
|
+
else console.error();
|
|
3214
|
+
let previewPrinted = false;
|
|
3215
|
+
for (const [xidStr, sendToArid] of sendToArids) {
|
|
3216
|
+
const xid = XID.fromURString(xidStr);
|
|
3217
|
+
const participantState = startState.participants.get(xidStr);
|
|
3218
|
+
if (participantState === void 0) throw new Error(`Participant state not found for ${xidStr}`);
|
|
3219
|
+
const participantName = registry.participant(xid)?.petName() ?? xidStr;
|
|
3220
|
+
let recipientDoc;
|
|
3221
|
+
if (xidStr === owner.xid().urString()) recipientDoc = owner.xidDocument();
|
|
3222
|
+
else {
|
|
3223
|
+
const record = registry.participant(xid);
|
|
3224
|
+
if (record === void 0) throw new Error(`Participant ${xidStr} not found in registry`);
|
|
3225
|
+
recipientDoc = record.xidDocument();
|
|
3226
|
+
}
|
|
3227
|
+
const request = buildShareRequestForParticipant(owner.xidDocument(), groupId, sessionId, participantState.shareArid, commitments);
|
|
3228
|
+
if (options.previewShare === true && !previewPrinted) {
|
|
3229
|
+
const preview = request.toEnvelope(validUntil, signerKeys, void 0);
|
|
3230
|
+
console.log(`# signRound2 preview for ${xidStr}`);
|
|
3231
|
+
console.log(preview.format());
|
|
3232
|
+
previewPrinted = true;
|
|
3233
|
+
}
|
|
3234
|
+
const sealedEnvelope = request.toEnvelopeForRecipients(validUntil, signerKeys, [recipientDoc]);
|
|
3235
|
+
await client.put(sendToArid, sealedEnvelope);
|
|
3236
|
+
if (isVerbose() || options.verbose === true) console.error(` ✓ ${participantName}`);
|
|
3237
|
+
}
|
|
3238
|
+
const displayPath = path.relative(cwd, commitmentsPath) || commitmentsPath;
|
|
3239
|
+
if (isVerbose() || options.verbose === true) {
|
|
3240
|
+
console.error();
|
|
3241
|
+
console.error(`Collected ${commitments.size} signInvite responses. Saved to ${displayPath}`);
|
|
3242
|
+
console.error(`Dispatched ${commitments.size} signRound2 requests.`);
|
|
3243
|
+
}
|
|
3244
|
+
return {
|
|
3245
|
+
accepted: commitments.size,
|
|
3246
|
+
rejected: 0,
|
|
3247
|
+
errors: errors.length,
|
|
3248
|
+
timeouts: 0
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
//#endregion
|
|
3254
|
+
//#region src/cmd/sign/coordinator/round2.ts
|
|
3255
|
+
/**
|
|
3256
|
+
* Sign coordinator round 2 command.
|
|
3257
|
+
*
|
|
3258
|
+
* Port of cmd/sign/coordinator/round2.rs from frost-hubert-rust.
|
|
3259
|
+
*
|
|
3260
|
+
* @module
|
|
3261
|
+
*/
|
|
3262
|
+
/**
|
|
3263
|
+
* Validate envelope and extract signature share data (for parallel fetch).
|
|
3264
|
+
*
|
|
3265
|
+
* Port of `validate_and_extract_share_response()` from cmd/sign/coordinator/round2.rs.
|
|
3266
|
+
*/
|
|
3267
|
+
function validateAndExtractShareResponse(envelope, _coordinatorKeys, expectedSender, expectedSessionId) {
|
|
3268
|
+
try {
|
|
3269
|
+
envelope.checkSubjectUnit();
|
|
3270
|
+
envelope.checkType("signRound2Response");
|
|
3271
|
+
const sessionObjects = envelope.objectsForPredicate("session");
|
|
3272
|
+
if (sessionObjects.length === 0) return { rejected: "Missing session in response" };
|
|
3273
|
+
const responseSession = ARID.fromTaggedCbor(sessionObjects[0].subject().tryLeaf());
|
|
3274
|
+
if (responseSession.urString() !== expectedSessionId.urString()) return { rejected: `Response session ${responseSession.urString()} does not match expected ${expectedSessionId.urString()}` };
|
|
3275
|
+
const participantObjects = envelope.objectsForPredicate("participant");
|
|
3276
|
+
if (participantObjects.length === 0) return { rejected: "Missing participant in response" };
|
|
3277
|
+
const participantXid = XID.fromTaggedCbor(participantObjects[0].subject().tryLeaf());
|
|
3278
|
+
if (participantXid.urString() !== expectedSender.urString()) return { rejected: `Unexpected response sender: ${participantXid.urString()} (expected ${expectedSender.urString()})` };
|
|
3279
|
+
const shareObjects = envelope.objectsForPredicate("signature_share");
|
|
3280
|
+
if (shareObjects.length === 0) return { rejected: "Missing signature_share in response" };
|
|
3281
|
+
const signatureShareJson = JSON$1.fromTaggedCbor(shareObjects[0].subject().tryLeaf());
|
|
3282
|
+
const signatureShare = deserializeSignatureShare(JSON.parse(signatureShareJson.toString()).share);
|
|
3283
|
+
const responseAridObjects = envelope.objectsForPredicate("response_arid");
|
|
3284
|
+
if (responseAridObjects.length === 0) return { rejected: "Missing response_arid in response" };
|
|
3285
|
+
return {
|
|
3286
|
+
signatureShare,
|
|
3287
|
+
finalizeArid: ARID.fromTaggedCbor(responseAridObjects[0].subject().tryLeaf())
|
|
3288
|
+
};
|
|
3289
|
+
} catch (error) {
|
|
3290
|
+
return { rejected: `Failed to parse response: ${error instanceof Error ? error.message : String(error)}` };
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
/**
|
|
3294
|
+
* Collect signature shares in parallel with progress display.
|
|
3295
|
+
*
|
|
3296
|
+
* Port of `collect_shares_parallel()` from cmd/sign/coordinator/round2.rs.
|
|
3297
|
+
*/
|
|
3298
|
+
async function collectSharesParallel(client, registry, commitmentsState, coordinator, sessionId, timeoutSeconds) {
|
|
3299
|
+
const requests = [];
|
|
3300
|
+
for (const [xidUr, entry] of commitmentsState.commitments) {
|
|
3301
|
+
const xid = XID.fromURString(xidUr);
|
|
3302
|
+
const name = registry.participant(xid)?.petName() ?? xid.urString();
|
|
3303
|
+
requests.push([
|
|
3304
|
+
xid,
|
|
3305
|
+
entry.shareArid,
|
|
3306
|
+
name
|
|
3307
|
+
]);
|
|
3308
|
+
}
|
|
3309
|
+
const coordinatorKeys = coordinator.inceptionPrivateKeys();
|
|
3310
|
+
if (!coordinatorKeys) throw new Error("Coordinator XID document has no inception private keys");
|
|
3311
|
+
const session = sessionId;
|
|
3312
|
+
return parallelFetch(client, requests, (envelope, xid) => {
|
|
3313
|
+
return validateAndExtractShareResponse(envelope, coordinatorKeys, xid, session);
|
|
3314
|
+
}, {
|
|
3315
|
+
timeoutSeconds,
|
|
3316
|
+
verbose: false
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
/**
|
|
3320
|
+
* Build a finalize event containing all signature shares.
|
|
3321
|
+
*
|
|
3322
|
+
* Port of `build_finalize_event()` from cmd/sign/coordinator/round2.rs.
|
|
3323
|
+
*/
|
|
3324
|
+
function buildFinalizeEvent(_sender, sessionId, signatureSharesByXid) {
|
|
3325
|
+
let content = SignFinalizeContent.new().addAssertion("session", sessionId);
|
|
3326
|
+
for (const [xidUr, share] of signatureSharesByXid) {
|
|
3327
|
+
const xid = XID.fromURString(xidUr);
|
|
3328
|
+
const shareHex = serializeSignatureShare(share);
|
|
3329
|
+
const shareJson = JSON$1.fromString(JSON.stringify({ share: shareHex }));
|
|
3330
|
+
const entry = Envelope.new(xid).addAssertion("share", shareJson);
|
|
3331
|
+
content = content.addAssertion("signature_share", entry);
|
|
3332
|
+
}
|
|
3333
|
+
return content;
|
|
3334
|
+
}
|
|
3335
|
+
/**
|
|
3336
|
+
* Aggregate signature shares and verify the result.
|
|
3337
|
+
*
|
|
3338
|
+
* Port of signature aggregation logic from cmd/sign/coordinator/round2.rs.
|
|
3339
|
+
*/
|
|
3340
|
+
function aggregateAndVerifySignature$1(signingCommitments, signatureSharesByIdentifier, publicKeyPackage, targetDigest) {
|
|
3341
|
+
const signatureBytes = serializeSignature(aggregateSignatures(createSigningPackage(signingCommitments, targetDigest), signatureSharesByIdentifier, publicKeyPackage));
|
|
3342
|
+
if (signatureBytes.length !== 64) throw new Error("Aggregated signature is not 64 bytes");
|
|
3343
|
+
const signature = Signature.ed25519FromData(signatureBytes);
|
|
3344
|
+
return {
|
|
3345
|
+
signature,
|
|
3346
|
+
signatureUr: signature.urString()
|
|
3347
|
+
};
|
|
3348
|
+
}
|
|
3349
|
+
/**
|
|
3350
|
+
* Persist final signing state to disk.
|
|
3351
|
+
*
|
|
3352
|
+
* Port of `persist_final_state()` from cmd/sign/coordinator/round2.rs.
|
|
3353
|
+
*/
|
|
3354
|
+
function persistSigningState(registryPath, groupId, sessionId, signature, signatureSharesByXid, finalizeArids) {
|
|
3355
|
+
const dir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
|
|
3356
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3357
|
+
const sharesJson = {};
|
|
3358
|
+
for (const [xidUr, share] of signatureSharesByXid) sharesJson[xidUr] = { share: serializeSignatureShare(share) };
|
|
3359
|
+
const finalizeJson = {};
|
|
3360
|
+
for (const [xidUr, arid] of finalizeArids) finalizeJson[xidUr] = arid.urString();
|
|
3361
|
+
const root = {
|
|
3362
|
+
group: groupId.urString(),
|
|
3363
|
+
session: sessionId.urString(),
|
|
3364
|
+
signature: signature.urString(),
|
|
3365
|
+
signature_shares: sharesJson,
|
|
3366
|
+
finalize_arids: finalizeJson
|
|
3367
|
+
};
|
|
3368
|
+
fs.writeFileSync(path.join(dir, "final.json"), JSON.stringify(root, null, 2));
|
|
3369
|
+
}
|
|
3370
|
+
/**
|
|
3371
|
+
* Load start state from disk.
|
|
3372
|
+
*
|
|
3373
|
+
* Port of `load_start_state()` from cmd/sign/coordinator/round2.rs.
|
|
3374
|
+
*/
|
|
3375
|
+
function loadStartState(registryPath, sessionId, groupHint) {
|
|
3376
|
+
const base = path.dirname(registryPath);
|
|
3377
|
+
const groupStateDir = path.join(base, "group-state");
|
|
3378
|
+
const candidatePaths = [];
|
|
3379
|
+
let groupDirs;
|
|
3380
|
+
if (groupHint) groupDirs = [[groupHint, path.join(groupStateDir, groupHint.hex())]];
|
|
3381
|
+
else {
|
|
3382
|
+
groupDirs = [];
|
|
3383
|
+
if (fs.existsSync(groupStateDir)) {
|
|
3384
|
+
for (const entry of fs.readdirSync(groupStateDir, { withFileTypes: true })) if (entry.isDirectory() && entry.name.length === 64 && /^[0-9a-f]+$/i.test(entry.name)) {
|
|
3385
|
+
const groupId = ARID.fromHex(entry.name);
|
|
3386
|
+
groupDirs.push([groupId, path.join(groupStateDir, entry.name)]);
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
for (const [groupId, groupDir] of groupDirs) {
|
|
3391
|
+
const candidate = path.join(groupDir, "signing", sessionId.hex(), "start.json");
|
|
3392
|
+
if (fs.existsSync(candidate)) candidatePaths.push([groupId, candidate]);
|
|
3393
|
+
}
|
|
3394
|
+
if (candidatePaths.length === 0) throw new Error("No sign start state found; run `frost sign coordinator start` first");
|
|
3395
|
+
if (candidatePaths.length > 1) throw new Error("Multiple signing sessions found; specify --group to disambiguate");
|
|
3396
|
+
const [groupId, statePath] = candidatePaths[0];
|
|
3397
|
+
const raw = JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
3398
|
+
const getStr = (key) => {
|
|
3399
|
+
const value = raw[key];
|
|
3400
|
+
if (typeof value !== "string") throw new Error(`Missing or invalid ${key} in start.json`);
|
|
3401
|
+
return value;
|
|
3402
|
+
};
|
|
3403
|
+
const sessionInState = parseAridUr(getStr("session_id"));
|
|
3404
|
+
const groupInState = parseAridUr(getStr("group"));
|
|
3405
|
+
if (sessionInState.urString() !== sessionId.urString()) throw new Error(`start.json session ${sessionInState.urString()} does not match requested session ${sessionId.urString()}`);
|
|
3406
|
+
if (groupInState.urString() !== groupId.urString()) throw new Error(`start.json group ${groupInState.urString()} does not match directory group ${groupId.urString()}`);
|
|
3407
|
+
const minSigners = raw["min_signers"];
|
|
3408
|
+
if (typeof minSigners !== "number") throw new Error("Missing min_signers in start.json");
|
|
3409
|
+
const participantsVal = raw["participants"];
|
|
3410
|
+
if (!participantsVal || typeof participantsVal !== "object") throw new Error("Missing participants in start.json");
|
|
3411
|
+
const participants = [];
|
|
3412
|
+
for (const xidStr of Object.keys(participantsVal)) participants.push(XID.fromURString(xidStr));
|
|
3413
|
+
participants.sort((a, b) => a.urString().localeCompare(b.urString()));
|
|
3414
|
+
return {
|
|
3415
|
+
groupId,
|
|
3416
|
+
minSigners,
|
|
3417
|
+
participants,
|
|
3418
|
+
targetUr: getStr("target")
|
|
3419
|
+
};
|
|
3420
|
+
}
|
|
3421
|
+
/**
|
|
3422
|
+
* Load commitments state from disk.
|
|
3423
|
+
*
|
|
3424
|
+
* Port of `load_commitments_state()` from cmd/sign/coordinator/round2.rs.
|
|
3425
|
+
*/
|
|
3426
|
+
function loadCommitmentsState(registryPath, groupId, sessionId) {
|
|
3427
|
+
const dir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
|
|
3428
|
+
const statePath = path.join(dir, "commitments.json");
|
|
3429
|
+
if (!fs.existsSync(statePath)) throw new Error(`Commitments not found at ${statePath}. Run \`frost sign coordinator collect\` first`);
|
|
3430
|
+
const raw = JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
3431
|
+
const getStr = (key) => {
|
|
3432
|
+
const value = raw[key];
|
|
3433
|
+
if (typeof value !== "string") throw new Error(`Missing or invalid ${key} in commitments.json`);
|
|
3434
|
+
return value;
|
|
3435
|
+
};
|
|
3436
|
+
const sessionInState = parseAridUr(getStr("session"));
|
|
3437
|
+
if (sessionInState.urString() !== sessionId.urString()) throw new Error(`commitments.json session ${sessionInState.urString()} does not match requested session ${sessionId.urString()}`);
|
|
3438
|
+
const commitmentsVal = raw["commitments"];
|
|
3439
|
+
if (!commitmentsVal || typeof commitmentsVal !== "object") throw new Error("Missing commitments map in commitments.json");
|
|
3440
|
+
const commitments = /* @__PURE__ */ new Map();
|
|
3441
|
+
for (const [xidStr, value] of Object.entries(commitmentsVal)) {
|
|
3442
|
+
const obj = value;
|
|
3443
|
+
const commitValue = obj["commitments"];
|
|
3444
|
+
if (!commitValue) throw new Error("Missing commitments value in commitments.json");
|
|
3445
|
+
const commitmentsDeserialized = deserializeSigningCommitments(commitValue);
|
|
3446
|
+
const shareAridRaw = obj["share_arid"];
|
|
3447
|
+
if (typeof shareAridRaw !== "string") throw new Error("Missing share_arid in commitments.json");
|
|
3448
|
+
const shareArid = parseAridUr(shareAridRaw);
|
|
3449
|
+
commitments.set(xidStr, {
|
|
3450
|
+
commitments: commitmentsDeserialized,
|
|
3451
|
+
shareArid
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
return { commitments };
|
|
3455
|
+
}
|
|
3456
|
+
/**
|
|
3457
|
+
* Load public key package from collected_finalize.json.
|
|
3458
|
+
*
|
|
3459
|
+
* Port of `load_public_key_package()` from cmd/sign/coordinator/round2.rs.
|
|
3460
|
+
*/
|
|
3461
|
+
function loadPublicKeyPackage$1(registryPath, groupId) {
|
|
3462
|
+
const base = path.dirname(registryPath);
|
|
3463
|
+
const pkgPath = path.join(base, "group-state", groupId.hex(), "collected_finalize.json");
|
|
3464
|
+
if (!fs.existsSync(pkgPath)) throw new Error(`collected_finalize.json not found at ${pkgPath}. Run \`frost dkg coordinator finalize collect\` first`);
|
|
3465
|
+
const raw = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
3466
|
+
const firstEntry = Object.values(raw)[0];
|
|
3467
|
+
if (!firstEntry) throw new Error("collected_finalize.json is empty");
|
|
3468
|
+
const publicKeyValue = firstEntry["public_key_package"];
|
|
3469
|
+
if (!publicKeyValue) throw new Error("public_key_package missing in collected_finalize.json");
|
|
3470
|
+
return deserializePublicKeyPackage(publicKeyValue);
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Build a map from XID to FROST identifier.
|
|
3474
|
+
*
|
|
3475
|
+
* Port of `xid_identifier_map()` from cmd/sign/coordinator/round2.rs.
|
|
3476
|
+
*/
|
|
3477
|
+
function xidIdentifierMap$2(participants) {
|
|
3478
|
+
const map = /* @__PURE__ */ new Map();
|
|
3479
|
+
for (let i = 0; i < participants.length; i++) {
|
|
3480
|
+
const identifier = identifierFromU16(i + 1);
|
|
3481
|
+
map.set(participants[i].urString(), identifier);
|
|
3482
|
+
}
|
|
3483
|
+
return map;
|
|
3484
|
+
}
|
|
3485
|
+
/**
|
|
3486
|
+
* Build signing commitments with identifiers.
|
|
3487
|
+
*
|
|
3488
|
+
* Port of `commitments_with_identifiers()` from cmd/sign/coordinator/round2.rs.
|
|
3489
|
+
*/
|
|
3490
|
+
function commitmentsWithIdentifiers$2(commitments, xidToIdentifier) {
|
|
3491
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
3492
|
+
for (const [xidUr, entry] of commitments) {
|
|
3493
|
+
const identifier = xidToIdentifier.get(xidUr);
|
|
3494
|
+
if (!identifier) throw new Error(`Unknown participant ${xidUr}`);
|
|
3495
|
+
mapped.set(identifier, entry.commitments);
|
|
3496
|
+
}
|
|
3497
|
+
return mapped;
|
|
3498
|
+
}
|
|
3499
|
+
/**
|
|
3500
|
+
* Execute the sign coordinator round 2 command.
|
|
3501
|
+
*
|
|
3502
|
+
* Collects signature shares, aggregates the signature, and posts finalize packages.
|
|
3503
|
+
*
|
|
3504
|
+
* Port of `CommandArgs::exec()` from cmd/sign/coordinator/round2.rs.
|
|
3505
|
+
*/
|
|
3506
|
+
async function round2$2(client, options, cwd) {
|
|
3507
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
3508
|
+
const registry = Registry.load(registryPath);
|
|
3509
|
+
const owner = registry.owner();
|
|
3510
|
+
if (!owner) throw new Error("Registry owner is required");
|
|
3511
|
+
const sessionId = parseAridUr(options.sessionId);
|
|
3512
|
+
const startState = loadStartState(registryPath, sessionId, options.groupId ? parseAridUr(options.groupId) : void 0);
|
|
3513
|
+
const groupId = startState.groupId;
|
|
3514
|
+
const groupRecord = registry.group(groupId);
|
|
3515
|
+
if (!groupRecord) throw new Error("Group not found in registry");
|
|
3516
|
+
if (groupRecord.coordinator().xid().urString() !== owner.xid().urString()) throw new Error(`Only the coordinator can finalize signing. Coordinator: ${groupRecord.coordinator().xid().urString()}, Owner: ${owner.xid().urString()}`);
|
|
3517
|
+
const commitmentsState = loadCommitmentsState(registryPath, groupId, sessionId);
|
|
3518
|
+
const xidToIdentifier = xidIdentifierMap$2(startState.participants);
|
|
3519
|
+
let signatureSharesByIdentifier;
|
|
3520
|
+
let signatureSharesByXid;
|
|
3521
|
+
let finalizeArids;
|
|
3522
|
+
if (options.parallel === true) {
|
|
3523
|
+
const collection = await collectSharesParallel(client, registry, commitmentsState, owner.xidDocument(), sessionId, options.timeoutSeconds);
|
|
3524
|
+
if (!collection.allSucceeded()) {
|
|
3525
|
+
if (collection.rejections.length > 0) {
|
|
3526
|
+
console.error("\nRejections:");
|
|
3527
|
+
for (const [xid, reason] of collection.rejections) console.error(` ${xid.urString()}: ${reason}`);
|
|
3528
|
+
}
|
|
3529
|
+
if (collection.errors.length > 0) {
|
|
3530
|
+
console.error("\nErrors:");
|
|
3531
|
+
for (const [xid, error] of collection.errors) console.error(` ${xid.urString()}: ${error}`);
|
|
3532
|
+
}
|
|
3533
|
+
if (collection.timeouts.length > 0) {
|
|
3534
|
+
console.error("\nTimeouts:");
|
|
3535
|
+
for (const xid of collection.timeouts) console.error(` ${xid.urString()}`);
|
|
3536
|
+
}
|
|
3537
|
+
throw new Error(`Signature share collection incomplete: ${collection.successes.length} succeeded, ${collection.rejections.length} rejected, ${collection.errors.length} errors, ${collection.timeouts.length} timeouts`);
|
|
3538
|
+
}
|
|
3539
|
+
signatureSharesByIdentifier = /* @__PURE__ */ new Map();
|
|
3540
|
+
signatureSharesByXid = /* @__PURE__ */ new Map();
|
|
3541
|
+
finalizeArids = /* @__PURE__ */ new Map();
|
|
3542
|
+
for (const [xid, data] of collection.successes) {
|
|
3543
|
+
const xidUr = xid.urString();
|
|
3544
|
+
const identifier = xidToIdentifier.get(xidUr);
|
|
3545
|
+
if (!identifier) throw new Error("Identifier mapping missing for participant");
|
|
3546
|
+
signatureSharesByIdentifier.set(identifier, data.signatureShare);
|
|
3547
|
+
signatureSharesByXid.set(xidUr, data.signatureShare);
|
|
3548
|
+
finalizeArids.set(xidUr, data.finalizeArid);
|
|
3549
|
+
}
|
|
3550
|
+
} else {
|
|
3551
|
+
if (options.verbose === true) console.error(`Collecting signature shares for session ${sessionId.urString()} from ${commitmentsState.commitments.size} participants...`);
|
|
3552
|
+
signatureSharesByIdentifier = /* @__PURE__ */ new Map();
|
|
3553
|
+
signatureSharesByXid = /* @__PURE__ */ new Map();
|
|
3554
|
+
finalizeArids = /* @__PURE__ */ new Map();
|
|
3555
|
+
for (const [xidUr, entry] of commitmentsState.commitments) {
|
|
3556
|
+
const xid = XID.fromURString(xidUr);
|
|
3557
|
+
const participantName = registry.participant(xid)?.petName() ?? xid.urString();
|
|
3558
|
+
const identifier = xidToIdentifier.get(xidUr);
|
|
3559
|
+
if (!identifier) throw new Error("Identifier mapping missing for participant");
|
|
3560
|
+
const envelope = await client.get(entry.shareArid, options.timeoutSeconds);
|
|
3561
|
+
if (!envelope) throw new Error(`Signature share response not found for ${participantName}`);
|
|
3562
|
+
const coordinatorKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
3563
|
+
if (!coordinatorKeys) throw new Error("Coordinator XID document has no inception private keys");
|
|
3564
|
+
const result = validateAndExtractShareResponse(envelope, coordinatorKeys, xid, sessionId);
|
|
3565
|
+
if ("rejected" in result) throw new Error(`Participant rejected signRound2: ${result.rejected}`);
|
|
3566
|
+
signatureSharesByIdentifier.set(identifier, result.signatureShare);
|
|
3567
|
+
signatureSharesByXid.set(xidUr, result.signatureShare);
|
|
3568
|
+
finalizeArids.set(xidUr, result.finalizeArid);
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3571
|
+
if (signatureSharesByIdentifier.size < startState.minSigners) throw new Error(`Only collected ${signatureSharesByIdentifier.size} signature shares, need at least ${startState.minSigners}`);
|
|
3572
|
+
const signingCommitments = commitmentsWithIdentifiers$2(commitmentsState.commitments, xidToIdentifier);
|
|
3573
|
+
const targetDigest = Envelope.fromURString(startState.targetUr).subject().digest().data();
|
|
3574
|
+
const publicKeyPackage = loadPublicKeyPackage$1(registryPath, groupId);
|
|
3575
|
+
const verifyingKey = signingKeyFromVerifying(publicKeyPackage.verifyingKey);
|
|
3576
|
+
const { signature, signatureUr } = aggregateAndVerifySignature$1(signingCommitments, signatureSharesByIdentifier, publicKeyPackage, targetDigest);
|
|
3577
|
+
if (verifyingKey.verify(signature, targetDigest) !== true) throw new Error("Aggregated signature failed verification against target digest");
|
|
3578
|
+
const signedEnvelopeUr = Envelope.fromURString(startState.targetUr).addAssertion("signed", signature).urString();
|
|
3579
|
+
persistSigningState(registryPath, groupId, sessionId, signature, signatureSharesByXid, finalizeArids);
|
|
3580
|
+
if (options.verbose === true) {
|
|
3581
|
+
console.error();
|
|
3582
|
+
console.error(`Aggregated signature for session ${sessionId.urString()} and prepared ${finalizeArids.size} finalize packages.`);
|
|
3583
|
+
console.error("Signature verified against target and group key.");
|
|
3584
|
+
}
|
|
3585
|
+
if (!owner.xidDocument().inceptionPrivateKeys()) throw new Error("Coordinator XID document has no signing keys");
|
|
3586
|
+
if (options.verbose === true) console.error(`Dispatching finalize packages to ${finalizeArids.size} participants...`);
|
|
3587
|
+
else console.error();
|
|
3588
|
+
const messages = [];
|
|
3589
|
+
let previewPrinted = false;
|
|
3590
|
+
for (const [xidUr, finalizeArid] of finalizeArids) {
|
|
3591
|
+
const participantXid = XID.fromURString(xidUr);
|
|
3592
|
+
const participant = registry.participant(participantXid);
|
|
3593
|
+
const participantName = participant?.petName() ?? xidUr;
|
|
3594
|
+
if (!(xidUr === owner.xid().urString() ? owner.xidDocument() : participant?.xidDocument())) throw new Error(`Participant ${xidUr} not found in registry`);
|
|
3595
|
+
const event = buildFinalizeEvent(owner.xidDocument(), sessionId, signatureSharesByXid);
|
|
3596
|
+
if (options.previewFinalize === true && !previewPrinted) {
|
|
3597
|
+
console.log(`# signFinalize preview for ${participantXid.urString()}`);
|
|
3598
|
+
console.log(event.envelope().format());
|
|
3599
|
+
previewPrinted = true;
|
|
3600
|
+
}
|
|
3601
|
+
const sealed = event.envelope();
|
|
3602
|
+
messages.push([
|
|
3603
|
+
participantXid,
|
|
3604
|
+
finalizeArid,
|
|
3605
|
+
sealed,
|
|
3606
|
+
participantName
|
|
3607
|
+
]);
|
|
3608
|
+
}
|
|
3609
|
+
if (options.parallel === true) {
|
|
3610
|
+
console.error();
|
|
3611
|
+
const results = await parallelSend(client, messages, options.verbose === true);
|
|
3612
|
+
const errors = [];
|
|
3613
|
+
for (const [xid, result] of results) if (result !== null) {
|
|
3614
|
+
const name = registry.participant(xid)?.petName() ?? xid.urString();
|
|
3615
|
+
errors.push(`${name}: ${result.message}`);
|
|
3616
|
+
}
|
|
3617
|
+
if (errors.length > 0) throw new Error(`Failed to send finalize packages: ${errors.join("; ")}`);
|
|
3618
|
+
} else for (const [_xid, finalizeArid, sealed, participantName] of messages) await putWithIndicator(client, finalizeArid, sealed, participantName, options.verbose ?? false);
|
|
3619
|
+
console.log(signatureUr);
|
|
3620
|
+
console.log(signedEnvelopeUr);
|
|
3621
|
+
return {
|
|
3622
|
+
signature: signatureUr,
|
|
3623
|
+
signedEnvelope: signedEnvelopeUr,
|
|
3624
|
+
accepted: signatureSharesByIdentifier.size,
|
|
3625
|
+
rejected: 0,
|
|
3626
|
+
errors: 0,
|
|
3627
|
+
timeouts: 0
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
//#endregion
|
|
3632
|
+
//#region src/cmd/sign/coordinator/index.ts
|
|
3633
|
+
var coordinator_exports = /* @__PURE__ */ __exportAll({
|
|
3634
|
+
buildSessionStateJson: () => buildSessionStateJson,
|
|
3635
|
+
buildShareRequestForParticipant: () => buildShareRequestForParticipant,
|
|
3636
|
+
buildSignInviteRequest: () => buildSignInviteRequest,
|
|
3637
|
+
collectCommitmentsParallel: () => collectCommitmentsParallel,
|
|
3638
|
+
createSessionArids: () => createSessionArids,
|
|
3639
|
+
dispatchShareRequestsParallel: () => dispatchShareRequestsParallel,
|
|
3640
|
+
gatherRecipientDocuments: () => gatherRecipientDocuments,
|
|
3641
|
+
invite: () => invite,
|
|
3642
|
+
loadEnvelopeFromPath: () => loadEnvelopeFromPath,
|
|
3643
|
+
persistCommitments: () => persistCommitments,
|
|
3644
|
+
persistSessionState: () => persistSessionState,
|
|
3645
|
+
round1: () => round1$2,
|
|
3646
|
+
round2: () => round2$2,
|
|
3647
|
+
updatePendingForShare: () => updatePendingForShare,
|
|
3648
|
+
validateAndExtractCommitResponse: () => validateAndExtractCommitResponse,
|
|
3649
|
+
validateCoordinator: () => validateCoordinator
|
|
3650
|
+
});
|
|
3651
|
+
|
|
3652
|
+
//#endregion
|
|
3653
|
+
//#region src/cmd/sign/participant/receive.ts
|
|
3654
|
+
/**
|
|
3655
|
+
* Sign participant receive command.
|
|
3656
|
+
*
|
|
3657
|
+
* Port of cmd/sign/participant/receive.rs from frost-hubert-rust.
|
|
3658
|
+
*
|
|
3659
|
+
* @module
|
|
3660
|
+
*/
|
|
3661
|
+
/**
|
|
3662
|
+
* Resolve sender from XID UR or pet name in registry.
|
|
3663
|
+
*
|
|
3664
|
+
* Port of `resolve_sender()` from cmd/dkg/common.rs lines 76-94.
|
|
3665
|
+
*/
|
|
3666
|
+
function resolveSenderFromInput(registry, input) {
|
|
3667
|
+
const trimmed = input.trim();
|
|
3668
|
+
if (trimmed === "") throw new Error("Sender is required");
|
|
3669
|
+
try {
|
|
3670
|
+
const { XID: XIDClass } = __require("@bcts/components");
|
|
3671
|
+
const xid = XIDClass.fromURString(trimmed);
|
|
3672
|
+
const record = registry.participant(xid);
|
|
3673
|
+
if (!record) throw new Error(`Sender with XID ${xid.urString()} not found`);
|
|
3674
|
+
return record.xidDocument();
|
|
3675
|
+
} catch {
|
|
3676
|
+
const result = registry.participantByPetName(trimmed);
|
|
3677
|
+
if (!result) throw new Error(`Sender with pet name '${trimmed}' not found`);
|
|
3678
|
+
return result[1].xidDocument();
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
/**
|
|
3682
|
+
* Resolve sign invite from ARID or envelope UR.
|
|
3683
|
+
*
|
|
3684
|
+
* Port of `resolve_sign_request()` from cmd/sign/participant/receive.rs lines 250-284.
|
|
3685
|
+
*/
|
|
3686
|
+
async function resolveSignInviteEnvelope(client, selection, request, timeout) {
|
|
3687
|
+
if (selection !== void 0 && client !== void 0) {
|
|
3688
|
+
try {
|
|
3689
|
+
const envelope = await getWithIndicator(client, parseAridUr(request), "Sign invite", timeout, false);
|
|
3690
|
+
if (envelope === void 0 || envelope === null) throw new Error("signInvite request not found in Hubert storage");
|
|
3691
|
+
return envelope;
|
|
3692
|
+
} catch {}
|
|
3693
|
+
if (timeout !== void 0) throw new Error("--timeout is only valid when retrieving requests from Hubert");
|
|
3694
|
+
return parseEnvelopeUr(request);
|
|
3695
|
+
}
|
|
3696
|
+
try {
|
|
3697
|
+
parseAridUr(request);
|
|
3698
|
+
throw new Error("Hubert storage parameters are required to retrieve requests by ARID");
|
|
3699
|
+
} catch (e) {
|
|
3700
|
+
if (e instanceof Error && e.message.includes("Hubert storage parameters")) throw e;
|
|
3701
|
+
}
|
|
3702
|
+
return parseEnvelopeUr(request);
|
|
3703
|
+
}
|
|
3704
|
+
/**
|
|
3705
|
+
* Get display name for sender from registry.
|
|
3706
|
+
*
|
|
3707
|
+
* Port of `resolve_sender_name()` from cmd/dkg/common.rs lines 96-116.
|
|
3708
|
+
*/
|
|
3709
|
+
function resolveSenderName(registry, senderXid) {
|
|
3710
|
+
const owner = registry.owner();
|
|
3711
|
+
if (owner?.xid().urString() === senderXid.urString()) return formatNameWithOwnerMarker(owner.petName() ?? senderXid.urString(), true);
|
|
3712
|
+
const record = registry.participant(senderXid);
|
|
3713
|
+
if (record) return formatNameWithOwnerMarker(record.petName() ?? record.xid().urString(), false);
|
|
3714
|
+
}
|
|
3715
|
+
/**
|
|
3716
|
+
* Format participant names with owner marker.
|
|
3717
|
+
*
|
|
3718
|
+
* Port of `format_participant_names()` from cmd/sign/participant/receive.rs lines 286-309.
|
|
3719
|
+
*/
|
|
3720
|
+
function formatParticipantNames(registry, participants, owner) {
|
|
3721
|
+
return participants.map((xid) => {
|
|
3722
|
+
const isOwner = xid.urString() === owner.xid().urString();
|
|
3723
|
+
let name;
|
|
3724
|
+
if (isOwner) name = owner.petName() ?? xid.urString();
|
|
3725
|
+
else name = registry.participant(xid)?.petName() ?? xid.urString();
|
|
3726
|
+
return formatNameWithOwnerMarker(name, isOwner);
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
/**
|
|
3730
|
+
* Execute the sign participant receive command.
|
|
3731
|
+
*
|
|
3732
|
+
* Fetches and validates a sign invite from the coordinator.
|
|
3733
|
+
*
|
|
3734
|
+
* Port of `CommandArgs::exec()` from cmd/sign/participant/receive.rs lines 56-247.
|
|
3735
|
+
*/
|
|
3736
|
+
async function receive(client, selection, options, cwd) {
|
|
3737
|
+
if (selection === void 0 && options.timeoutSeconds !== void 0) throw new Error("--timeout requires Hubert storage parameters");
|
|
3738
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
3739
|
+
const registry = Registry.load(registryPath);
|
|
3740
|
+
const owner = registry.owner();
|
|
3741
|
+
if (!owner) throw new Error("Registry owner with private keys is required");
|
|
3742
|
+
let expectedSender;
|
|
3743
|
+
if (options.sender !== void 0 && options.sender !== "") expectedSender = resolveSenderFromInput(registry, options.sender);
|
|
3744
|
+
const envelope = await resolveSignInviteEnvelope(client, selection, options.request, options.timeoutSeconds);
|
|
3745
|
+
const now = CborDate.now();
|
|
3746
|
+
const recipientKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
3747
|
+
if (recipientKeys === null || recipientKeys === void 0) throw new Error("Owner XID document has no inception private keys");
|
|
3748
|
+
const { SealedRequest: SealedRequestClass } = __require("@bcts/gstp");
|
|
3749
|
+
const sealedRequest = SealedRequestClass.tryFromEnvelope(envelope, void 0, now, recipientKeys);
|
|
3750
|
+
const senderXid = sealedRequest.sender().xid();
|
|
3751
|
+
if (expectedSender !== void 0) {
|
|
3752
|
+
if (senderXid.urString() !== expectedSender.xid().urString()) throw new Error(`Request sender does not match expected sender (got ${senderXid.urString()}, expected ${expectedSender.xid().urString()})`);
|
|
3753
|
+
} else {
|
|
3754
|
+
const knownOwner = owner.xid().urString() === senderXid.urString();
|
|
3755
|
+
const knownParticipant = registry.participant(senderXid) !== void 0;
|
|
3756
|
+
if (!knownOwner && !knownParticipant) throw new Error(`Request sender not found in registry: ${senderXid.urString()}`);
|
|
3757
|
+
}
|
|
3758
|
+
const { Function: FunctionClass } = __require("@bcts/envelope");
|
|
3759
|
+
const requestFunction = sealedRequest.function();
|
|
3760
|
+
const expectedFunction = FunctionClass.from("signInvite");
|
|
3761
|
+
if (!(requestFunction.equals !== void 0 ? requestFunction.equals(expectedFunction) : String(requestFunction) === String(expectedFunction))) throw new Error(`Unexpected request function: ${String(requestFunction)}`);
|
|
3762
|
+
if (sealedRequest.extractObjectForParameter("validUntil") <= now) throw new Error("signInvite request has expired");
|
|
3763
|
+
const groupId = sealedRequest.extractObjectForParameter("group");
|
|
3764
|
+
const sessionId = sealedRequest.extractObjectForParameter("session");
|
|
3765
|
+
const minSigners = Number(sealedRequest.extractObjectForParameter("minSigners"));
|
|
3766
|
+
const participantEntries = sealedRequest.objectsForParameter("participant");
|
|
3767
|
+
const participants = [];
|
|
3768
|
+
let responseArid;
|
|
3769
|
+
for (const entry of participantEntries) {
|
|
3770
|
+
const xid = entry.extractSubject();
|
|
3771
|
+
if (xid.urString() === owner.xid().urString()) responseArid = entry.objectForPredicate("response_arid").decryptToRecipient(recipientKeys).extractSubject();
|
|
3772
|
+
participants.push(xid);
|
|
3773
|
+
}
|
|
3774
|
+
if (participants.length === 0) throw new Error("signInvite request contains no participants");
|
|
3775
|
+
if (minSigners < 2) throw new Error("minSigners must be at least 2");
|
|
3776
|
+
if (minSigners > participants.length) throw new Error("minSigners exceeds participant count");
|
|
3777
|
+
if (!participants.some((p) => p.urString() === owner.xid().urString())) throw new Error("signInvite request does not include this participant");
|
|
3778
|
+
if (responseArid === void 0) throw new Error("signInvite request missing response ARID");
|
|
3779
|
+
participants.sort((a, b) => a.urString().localeCompare(b.urString()));
|
|
3780
|
+
const targetEnvelope = sealedRequest.objectForParameter("target");
|
|
3781
|
+
const coordinatorName = resolveSenderName(registry, senderXid) ?? senderXid.urString();
|
|
3782
|
+
const participantNames = formatParticipantNames(registry, participants, owner);
|
|
3783
|
+
console.log(`Group: ${groupId.urString()}`);
|
|
3784
|
+
console.log(`Coordinator: ${coordinatorName}`);
|
|
3785
|
+
console.log(`Min signers: ${minSigners}`);
|
|
3786
|
+
console.log(`Participants: ${participantNames.join(", ")}`);
|
|
3787
|
+
console.log("Target:");
|
|
3788
|
+
console.log(targetEnvelope.format());
|
|
3789
|
+
console.log(sessionId.urString());
|
|
3790
|
+
const stateDir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
|
|
3791
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
3792
|
+
const root = {
|
|
3793
|
+
request_envelope: envelope.urString(),
|
|
3794
|
+
group: groupId.urString(),
|
|
3795
|
+
session: sessionId.urString(),
|
|
3796
|
+
coordinator: senderXid.urString(),
|
|
3797
|
+
min_signers: minSigners,
|
|
3798
|
+
response_arid: responseArid.urString(),
|
|
3799
|
+
participants: participants.map((xid) => xid.urString()),
|
|
3800
|
+
target: targetEnvelope.urString()
|
|
3801
|
+
};
|
|
3802
|
+
fs.writeFileSync(path.join(stateDir, "sign_receive.json"), JSON.stringify(root, null, 2));
|
|
3803
|
+
return {
|
|
3804
|
+
sessionId: sessionId.urString(),
|
|
3805
|
+
groupId: groupId.urString(),
|
|
3806
|
+
targetUr: targetEnvelope.urString(),
|
|
3807
|
+
coordinatorName,
|
|
3808
|
+
minSigners,
|
|
3809
|
+
participantNames
|
|
3810
|
+
};
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
//#endregion
|
|
3814
|
+
//#region src/cmd/sign/participant/round1.ts
|
|
3815
|
+
/**
|
|
3816
|
+
* Sign participant round 1 command.
|
|
3817
|
+
*
|
|
3818
|
+
* Port of cmd/sign/participant/round1.rs from frost-hubert-rust.
|
|
3819
|
+
*
|
|
3820
|
+
* @module
|
|
3821
|
+
*/
|
|
3822
|
+
/**
|
|
3823
|
+
* Load receive state from persisted sign_receive.json.
|
|
3824
|
+
*
|
|
3825
|
+
* Port of `load_receive_state()` from cmd/sign/participant/round1.rs lines 285-411.
|
|
3826
|
+
*/
|
|
3827
|
+
function loadReceiveState$2(registryPath, sessionId, groupHint, registry) {
|
|
3828
|
+
const base = path.dirname(registryPath);
|
|
3829
|
+
const groupStateDir = path.join(base, "group-state");
|
|
3830
|
+
let groupDirs;
|
|
3831
|
+
if (groupHint !== void 0) groupDirs = [[groupHint, path.join(groupStateDir, groupHint.hex())]];
|
|
3832
|
+
else {
|
|
3833
|
+
groupDirs = [];
|
|
3834
|
+
if (fs.existsSync(groupStateDir)) {
|
|
3835
|
+
for (const entry of fs.readdirSync(groupStateDir, { withFileTypes: true })) if (entry.isDirectory()) {
|
|
3836
|
+
const dirName = entry.name;
|
|
3837
|
+
if (dirName.length === 64 && /^[0-9a-fA-F]+$/.test(dirName)) {
|
|
3838
|
+
const groupId = ARID.fromHex(dirName);
|
|
3839
|
+
groupDirs.push([groupId, path.join(groupStateDir, dirName)]);
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
const candidates = [];
|
|
3845
|
+
for (const [groupId, groupDir] of groupDirs) {
|
|
3846
|
+
const candidate = path.join(groupDir, "signing", sessionId.hex(), "sign_receive.json");
|
|
3847
|
+
if (fs.existsSync(candidate)) candidates.push([groupId, candidate]);
|
|
3848
|
+
}
|
|
3849
|
+
if (candidates.length === 0) throw new Error("No sign_receive.json found for this session; run `frost sign participant receive` first");
|
|
3850
|
+
if (candidates.length > 1) throw new Error("Multiple groups contain this session; use --group to disambiguate");
|
|
3851
|
+
const [groupId, filePath] = candidates[0];
|
|
3852
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
3853
|
+
const sessionInState = parseAridUr(raw.session);
|
|
3854
|
+
if (sessionInState.urString() !== sessionId.urString()) throw new Error(`Session ${sessionInState.urString()} in sign_receive.json does not match requested session ${sessionId.urString()}`);
|
|
3855
|
+
const responseArid = parseAridUr(raw.response_arid);
|
|
3856
|
+
const targetUr = raw.target;
|
|
3857
|
+
const coordinatorUr = raw.coordinator;
|
|
3858
|
+
const coordinatorXid = XID.fromURString(coordinatorUr);
|
|
3859
|
+
let coordinatorDoc;
|
|
3860
|
+
const participantRecord = registry.participant(coordinatorXid);
|
|
3861
|
+
if (participantRecord !== null && participantRecord !== void 0) coordinatorDoc = participantRecord.xidDocument();
|
|
3862
|
+
else {
|
|
3863
|
+
const owner = registry.owner();
|
|
3864
|
+
if (owner?.xid().urString() === coordinatorXid.urString()) coordinatorDoc = owner.xidDocument();
|
|
3865
|
+
else throw new Error(`Coordinator ${coordinatorXid.urString()} not found in registry and cannot resolve encryption key`);
|
|
3866
|
+
}
|
|
3867
|
+
const requestEnvelope = Envelope.fromURString(raw.request_envelope);
|
|
3868
|
+
const participants = raw.participants.map((s) => XID.fromURString(s));
|
|
3869
|
+
return {
|
|
3870
|
+
groupId,
|
|
3871
|
+
coordinatorDoc,
|
|
3872
|
+
responseArid,
|
|
3873
|
+
targetUr,
|
|
3874
|
+
participants,
|
|
3875
|
+
requestEnvelope
|
|
3876
|
+
};
|
|
3877
|
+
}
|
|
3878
|
+
/**
|
|
3879
|
+
* Validate the commit request from persisted state.
|
|
3880
|
+
*
|
|
3881
|
+
* Port of request validation in `CommandArgs::exec()` from cmd/sign/participant/round1.rs lines 100-138.
|
|
3882
|
+
*/
|
|
3883
|
+
function validateCommitRequest(receiveState, sessionId, ownerXid, ownerPrivateKeys) {
|
|
3884
|
+
const now = CborDate.now();
|
|
3885
|
+
const sealedRequest = SealedRequest.tryFromEnvelope(receiveState.requestEnvelope, void 0, now.datetime(), ownerPrivateKeys);
|
|
3886
|
+
if (!sealedRequest.request().function().equals(Function.fromString("signInvite"))) throw new Error(`Unexpected request function: ${String(sealedRequest.request().function())}`);
|
|
3887
|
+
if (sealedRequest.request().id().urString() !== sessionId.urString()) throw new Error(`Session ID mismatch (state ${sessionId.urString()}, request ${sealedRequest.request().id().urString()})`);
|
|
3888
|
+
const requestGroup = sealedRequest.extractObjectForParameter("group");
|
|
3889
|
+
if (requestGroup.urString() !== receiveState.groupId.urString()) throw new Error(`Group ID mismatch (state ${receiveState.groupId.urString()}, request ${requestGroup.urString()})`);
|
|
3890
|
+
if (!receiveState.participants.map((p) => p.urString()).includes(ownerXid.urString())) throw new Error("Persisted signInvite request does not include this participant");
|
|
3891
|
+
return sealedRequest;
|
|
3892
|
+
}
|
|
3893
|
+
/**
|
|
3894
|
+
* Build the response body envelope.
|
|
3895
|
+
*
|
|
3896
|
+
* Port of response body building from cmd/sign/participant/round1.rs lines 191-195.
|
|
3897
|
+
*/
|
|
3898
|
+
function buildResponseBody$1(sessionId, commitments, responseArid) {
|
|
3899
|
+
const serializedCommitments = serializeSigningCommitments(commitments);
|
|
3900
|
+
const jsonStr = JSON.stringify(serializedCommitments);
|
|
3901
|
+
const jsonBytes = new TextEncoder().encode(jsonStr);
|
|
3902
|
+
const commitmentsJson = JSON$1.fromData(jsonBytes);
|
|
3903
|
+
return Envelope.unit().addType("signRound1Response").addAssertion("session", sessionId).addAssertion("commitments", commitmentsJson.taggedCborData()).addAssertion("response_arid", responseArid);
|
|
3904
|
+
}
|
|
3905
|
+
/**
|
|
3906
|
+
* Persist commit state (nonces and commitments) to disk.
|
|
3907
|
+
*
|
|
3908
|
+
* Port of `persist_commit_state()` from cmd/sign/participant/round1.rs lines 413-461.
|
|
3909
|
+
*/
|
|
3910
|
+
function persistCommitState(registryPath, groupId, sessionId, receiveState, signingNonces, signingCommitments, targetEnvelope, nextShareArid) {
|
|
3911
|
+
const dir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
|
|
3912
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
3913
|
+
const serializedNonces = serializeSigningNonces(signingNonces);
|
|
3914
|
+
const serializedCommitments = serializeSigningCommitments(signingCommitments);
|
|
3915
|
+
const commitState = {
|
|
3916
|
+
session: sessionId.urString(),
|
|
3917
|
+
response_arid: receiveState.responseArid.urString(),
|
|
3918
|
+
next_share_arid: nextShareArid.urString(),
|
|
3919
|
+
target: targetEnvelope.urString(),
|
|
3920
|
+
signing_nonces: serializedNonces,
|
|
3921
|
+
signing_commitments: serializedCommitments
|
|
3922
|
+
};
|
|
3923
|
+
fs.writeFileSync(path.join(dir, "commit.json"), JSON.stringify(commitState, null, 2));
|
|
3924
|
+
}
|
|
3925
|
+
/**
|
|
3926
|
+
* Execute the sign participant round 1 command.
|
|
3927
|
+
*
|
|
3928
|
+
* Responds to the sign invite with signing commitments.
|
|
3929
|
+
*
|
|
3930
|
+
* Port of `CommandArgs::exec()` from cmd/sign/participant/round1.rs lines 58-273.
|
|
3931
|
+
*/
|
|
3932
|
+
async function round1$1(_client, options, cwd) {
|
|
3933
|
+
if (options.storageSelection === void 0 && options.preview !== true) throw new Error("Hubert storage is required for sign commit");
|
|
3934
|
+
if (options.storageSelection !== void 0 && options.preview === true) throw new Error("--preview cannot be used with Hubert storage options");
|
|
3935
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
3936
|
+
const registry = Registry.load(registryPath);
|
|
3937
|
+
const owner = registry.owner();
|
|
3938
|
+
if (!owner) throw new Error("Registry owner is required");
|
|
3939
|
+
const sessionId = parseAridUr(options.sessionId);
|
|
3940
|
+
const receiveState = loadReceiveState$2(registryPath, sessionId, options.groupId !== void 0 ? parseAridUr(options.groupId) : void 0, registry);
|
|
3941
|
+
const groupId = receiveState.groupId;
|
|
3942
|
+
const groupRecord = registry.group(groupId);
|
|
3943
|
+
if (groupRecord === null || groupRecord === void 0) throw new Error("Group not found in registry");
|
|
3944
|
+
const ownerKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
3945
|
+
if (ownerKeys === void 0) throw new Error("Owner XID document has no private keys");
|
|
3946
|
+
const sealedRequest = validateCommitRequest(receiveState, sessionId, owner.xid(), ownerKeys);
|
|
3947
|
+
const contributions = groupRecord.contributions();
|
|
3948
|
+
if (contributions === null || contributions === void 0) throw new Error("Key package path not found; did you finish DKG?");
|
|
3949
|
+
const keyPackagePath = contributions.keyPackage;
|
|
3950
|
+
if (keyPackagePath === void 0) throw new Error("Key package path not found; did you finish DKG?");
|
|
3951
|
+
const keyPackage = deserializeKeyPackage(JSON.parse(fs.readFileSync(keyPackagePath, "utf-8")).key_package);
|
|
3952
|
+
const targetEnvelope = Envelope.fromURString(receiveState.targetUr);
|
|
3953
|
+
const signerPrivateKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
3954
|
+
if (signerPrivateKeys === void 0) throw new Error("Owner XID document has no signing keys");
|
|
3955
|
+
let sealedResponse;
|
|
3956
|
+
let nextShareArid;
|
|
3957
|
+
if (options.rejectReason !== void 0) {
|
|
3958
|
+
const errorBody = Envelope.new("signCommitReject").addAssertion("group", groupId).addAssertion("session", sessionId).addAssertion("reason", options.rejectReason);
|
|
3959
|
+
sealedResponse = SealedResponse.newFailure(sealedRequest.request().id(), owner.xidDocument()).withError(errorBody).withPeerContinuation(sealedRequest.peerContinuation());
|
|
3960
|
+
} else {
|
|
3961
|
+
const [signingNonces, signingCommitments] = signingRound1(keyPackage, createRng());
|
|
3962
|
+
const nextShare = ARID.new();
|
|
3963
|
+
nextShareArid = nextShare;
|
|
3964
|
+
const responseBody = buildResponseBody$1(sessionId, signingCommitments, nextShare);
|
|
3965
|
+
if (options.preview !== true) {
|
|
3966
|
+
persistCommitState(registryPath, groupId, sessionId, receiveState, signingNonces, signingCommitments, targetEnvelope, nextShare);
|
|
3967
|
+
const groupRecordMut = registry.group(groupId);
|
|
3968
|
+
if (groupRecordMut !== null && groupRecordMut !== void 0) {
|
|
3969
|
+
groupRecordMut.setListeningAtArid(nextShare);
|
|
3970
|
+
registry.save(registryPath);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3973
|
+
sealedResponse = SealedResponse.newSuccess(sealedRequest.request().id(), owner.xidDocument()).withResult(responseBody).withPeerContinuation(sealedRequest.peerContinuation());
|
|
3974
|
+
}
|
|
3975
|
+
if (options.preview === true) {
|
|
3976
|
+
const envelopeUr = sealedResponse.toEnvelope(void 0, signerPrivateKeys, void 0).urString();
|
|
3977
|
+
console.log(envelopeUr);
|
|
3978
|
+
return {
|
|
3979
|
+
accepted: options.rejectReason === void 0,
|
|
3980
|
+
envelopeUr
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
const validUntil = new Date(Date.now() + 3600 * 1e3);
|
|
3984
|
+
const responseEnvelope = sealedResponse.toEnvelope(validUntil, signerPrivateKeys, receiveState.coordinatorDoc);
|
|
3985
|
+
if (options.storageSelection === void 0) throw new Error("Storage selection is required to post response");
|
|
3986
|
+
const client = await createStorageClient(options.storageSelection);
|
|
3987
|
+
if (options.verbose === true) console.error(`Posting signInvite response to ${receiveState.responseArid.urString()}`);
|
|
3988
|
+
await putWithIndicator(client, receiveState.responseArid, responseEnvelope, "Commitments", options.verbose ?? false);
|
|
3989
|
+
if (options.rejectReason !== void 0) {
|
|
3990
|
+
const groupRecordMut = registry.group(groupId);
|
|
3991
|
+
if (groupRecordMut !== null && groupRecordMut !== void 0) {
|
|
3992
|
+
groupRecordMut.clearListeningAtArid();
|
|
3993
|
+
registry.save(registryPath);
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
const result = { accepted: options.rejectReason === void 0 };
|
|
3997
|
+
if (nextShareArid !== void 0) result.listeningArid = nextShareArid.urString();
|
|
3998
|
+
return result;
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
//#endregion
|
|
4002
|
+
//#region src/cmd/sign/participant/round2.ts
|
|
4003
|
+
/**
|
|
4004
|
+
* Sign participant round 2 command.
|
|
4005
|
+
*
|
|
4006
|
+
* Port of cmd/sign/participant/round2.rs from frost-hubert-rust.
|
|
4007
|
+
*
|
|
4008
|
+
* @module
|
|
4009
|
+
*/
|
|
4010
|
+
/**
|
|
4011
|
+
* Load receive state from sign_receive.json.
|
|
4012
|
+
*
|
|
4013
|
+
* Port of `load_receive_state()` from cmd/sign/participant/round2.rs.
|
|
4014
|
+
*/
|
|
4015
|
+
function loadReceiveState$1(registryPath, sessionId, groupHint) {
|
|
4016
|
+
const base = path.dirname(registryPath);
|
|
4017
|
+
const groupStateDir = path.join(base, "group-state");
|
|
4018
|
+
let groupDirs;
|
|
4019
|
+
if (groupHint) groupDirs = [[groupHint, path.join(groupStateDir, groupHint.hex())]];
|
|
4020
|
+
else {
|
|
4021
|
+
groupDirs = [];
|
|
4022
|
+
if (fs.existsSync(groupStateDir)) {
|
|
4023
|
+
for (const entry of fs.readdirSync(groupStateDir, { withFileTypes: true })) if (entry.isDirectory() && entry.name.length === 64 && /^[0-9a-f]+$/i.test(entry.name)) {
|
|
4024
|
+
const groupId = ARID.fromHex(entry.name);
|
|
4025
|
+
groupDirs.push([groupId, path.join(groupStateDir, entry.name)]);
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
const candidates = [];
|
|
4030
|
+
for (const [groupId, groupDir] of groupDirs) {
|
|
4031
|
+
const candidate = path.join(groupDir, "signing", sessionId.hex(), "sign_receive.json");
|
|
4032
|
+
if (fs.existsSync(candidate)) candidates.push([groupId, candidate]);
|
|
4033
|
+
}
|
|
4034
|
+
if (candidates.length === 0) throw new Error("No sign_receive.json found for this session; run `frost sign participant receive` first");
|
|
4035
|
+
if (candidates.length > 1) throw new Error("Multiple groups contain this session; use --group to disambiguate");
|
|
4036
|
+
const [groupId, statePath] = candidates[0];
|
|
4037
|
+
const raw = JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
4038
|
+
const getStr = (key) => {
|
|
4039
|
+
const value = raw[key];
|
|
4040
|
+
if (typeof value !== "string") throw new Error(`Missing or invalid ${key} in sign_receive.json`);
|
|
4041
|
+
return value;
|
|
4042
|
+
};
|
|
4043
|
+
const sessionInState = parseAridUr(getStr("session"));
|
|
4044
|
+
if (sessionInState.urString() !== sessionId.urString()) throw new Error(`Session ${sessionInState.urString()} in sign_receive.json does not match requested session ${sessionId.urString()}`);
|
|
4045
|
+
const groupInState = parseAridUr(getStr("group"));
|
|
4046
|
+
if (groupInState.urString() !== groupId.urString()) throw new Error(`Group ${groupInState.urString()} in sign_receive.json does not match directory group ${groupId.urString()}`);
|
|
4047
|
+
const participantsVal = raw["participants"];
|
|
4048
|
+
if (!participantsVal || !Array.isArray(participantsVal)) throw new Error("Missing participants in sign_receive.json");
|
|
4049
|
+
const participants = [];
|
|
4050
|
+
for (const entry of participantsVal) {
|
|
4051
|
+
if (typeof entry !== "string") throw new Error("Invalid participant entry in sign_receive.json");
|
|
4052
|
+
participants.push(XID.fromURString(entry));
|
|
4053
|
+
}
|
|
4054
|
+
const minSigners = raw["min_signers"];
|
|
4055
|
+
if (typeof minSigners !== "number") throw new Error("Missing min_signers in sign_receive.json");
|
|
4056
|
+
return {
|
|
4057
|
+
groupId,
|
|
4058
|
+
participants,
|
|
4059
|
+
minSigners,
|
|
4060
|
+
targetUr: getStr("target")
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* Load commit state from commit.json (includes nonces).
|
|
4065
|
+
*
|
|
4066
|
+
* Port of `load_commit_state()` from cmd/sign/participant/round2.rs.
|
|
4067
|
+
*/
|
|
4068
|
+
function loadCommitState(registryPath, groupId, sessionId) {
|
|
4069
|
+
const dir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
|
|
4070
|
+
const statePath = path.join(dir, "commit.json");
|
|
4071
|
+
if (!fs.existsSync(statePath)) throw new Error(`Commit state not found at ${statePath}. Run \`frost sign participant commit\` first.`);
|
|
4072
|
+
const raw = JSON.parse(fs.readFileSync(statePath, "utf-8"));
|
|
4073
|
+
const getStr = (key) => {
|
|
4074
|
+
const value = raw[key];
|
|
4075
|
+
if (typeof value !== "string") throw new Error(`Missing or invalid ${key} in commit.json`);
|
|
4076
|
+
return value;
|
|
4077
|
+
};
|
|
4078
|
+
const sessionInState = parseAridUr(getStr("session"));
|
|
4079
|
+
if (sessionInState.urString() !== sessionId.urString()) throw new Error(`Session ${sessionInState.urString()} in commit.json does not match requested session ${sessionId.urString()}`);
|
|
4080
|
+
const nextShareArid = parseAridUr(getStr("next_share_arid"));
|
|
4081
|
+
const targetUr = getStr("target");
|
|
4082
|
+
const noncesRaw = raw["signing_nonces"];
|
|
4083
|
+
if (!noncesRaw) throw new Error("Missing signing_nonces in commit.json");
|
|
4084
|
+
const hidingNonce = Nonce.deserialize(Ed25519Sha512, serde.hexToBytes(noncesRaw["hiding"]));
|
|
4085
|
+
const bindingNonce = Nonce.deserialize(Ed25519Sha512, serde.hexToBytes(noncesRaw["binding"]));
|
|
4086
|
+
const signingNonces = SigningNonces.fromNonces(Ed25519Sha512, hidingNonce, bindingNonce);
|
|
4087
|
+
const commitmentsRaw = raw["signing_commitments"];
|
|
4088
|
+
if (!commitmentsRaw) throw new Error("Missing signing_commitments in commit.json");
|
|
4089
|
+
return {
|
|
4090
|
+
nextShareArid,
|
|
4091
|
+
targetUr,
|
|
4092
|
+
signingNonces,
|
|
4093
|
+
signingCommitments: deserializeSigningCommitments(commitmentsRaw)
|
|
4094
|
+
};
|
|
4095
|
+
}
|
|
4096
|
+
/**
|
|
4097
|
+
* Validate the incoming GSTP request.
|
|
4098
|
+
*
|
|
4099
|
+
* Port of request validation logic from cmd/sign/participant/round2.rs.
|
|
4100
|
+
*/
|
|
4101
|
+
function validateShareRequest(sealedRequest, sessionId, expectedCoordinator) {
|
|
4102
|
+
const expectedFunction = Function.fromString("signRound2");
|
|
4103
|
+
if (sealedRequest.function().equals(expectedFunction) !== true) throw new Error(`Unexpected request function: ${String(sealedRequest.function())}`);
|
|
4104
|
+
if (sealedRequest.id().urString() !== sessionId.urString()) throw new Error(`Session ID mismatch (request ${sealedRequest.id().urString()}, expected ${sessionId.urString()})`);
|
|
4105
|
+
if (sealedRequest.sender().xid().urString() !== expectedCoordinator.urString()) throw new Error(`Unexpected request sender: ${sealedRequest.sender().xid().urString()} (expected coordinator ${expectedCoordinator.urString()})`);
|
|
4106
|
+
}
|
|
4107
|
+
/**
|
|
4108
|
+
* Extract all commitments from the signRound2 request.
|
|
4109
|
+
*
|
|
4110
|
+
* Port of `parse_commitments()` from cmd/sign/participant/round2.rs.
|
|
4111
|
+
*/
|
|
4112
|
+
function extractCommitments(sealedRequest, receiveState) {
|
|
4113
|
+
const commitments = /* @__PURE__ */ new Map();
|
|
4114
|
+
const commitmentObjects = sealedRequest.objectsForParameter("commitment");
|
|
4115
|
+
for (const entry of commitmentObjects) {
|
|
4116
|
+
const xid = XID.fromTaggedCbor(entry.subject().tryLeaf());
|
|
4117
|
+
const commitmentsObjects = entry.objectsForPredicate("commitments");
|
|
4118
|
+
if (commitmentsObjects.length === 0) throw new Error(`Missing commitments for participant ${xid.urString()}`);
|
|
4119
|
+
const commitmentsJson = JSON$1.fromTaggedCbor(commitmentsObjects[0].subject().tryLeaf());
|
|
4120
|
+
const signingCommitments = deserializeSigningCommitments(JSON.parse(commitmentsJson.asStr()));
|
|
4121
|
+
const xidUr = xid.urString();
|
|
4122
|
+
if (commitments.has(xidUr)) throw new Error(`Duplicate commitments for participant ${xidUr}`);
|
|
4123
|
+
commitments.set(xidUr, signingCommitments);
|
|
4124
|
+
}
|
|
4125
|
+
if (commitments.size === 0) throw new Error("signRound2 request contains no commitments");
|
|
4126
|
+
const expectedSet = new Set(receiveState.participants.map((p) => p.urString()));
|
|
4127
|
+
const actualSet = new Set(commitments.keys());
|
|
4128
|
+
const missing = [];
|
|
4129
|
+
const extra = [];
|
|
4130
|
+
for (const xid of expectedSet) if (!actualSet.has(xid)) missing.push(xid);
|
|
4131
|
+
for (const xid of actualSet) if (!expectedSet.has(xid)) extra.push(xid);
|
|
4132
|
+
if (missing.length > 0 || extra.length > 0) throw new Error(`signRound2 commitments do not match session participants (missing: ${missing.join(", ")}; extra: ${extra.join(", ")})`);
|
|
4133
|
+
return commitments;
|
|
4134
|
+
}
|
|
4135
|
+
/**
|
|
4136
|
+
* Build a map from XID to FROST identifier (sorted participant order).
|
|
4137
|
+
*
|
|
4138
|
+
* Port of `xid_identifier_map()` from cmd/sign/participant/round2.rs.
|
|
4139
|
+
*/
|
|
4140
|
+
function xidIdentifierMap$1(participants) {
|
|
4141
|
+
const map = /* @__PURE__ */ new Map();
|
|
4142
|
+
for (let i = 0; i < participants.length; i++) {
|
|
4143
|
+
const identifier = identifierFromU16(i + 1);
|
|
4144
|
+
map.set(participants[i].urString(), identifier);
|
|
4145
|
+
}
|
|
4146
|
+
return map;
|
|
4147
|
+
}
|
|
4148
|
+
/**
|
|
4149
|
+
* Build signing commitments with identifiers.
|
|
4150
|
+
*
|
|
4151
|
+
* Port of `commitments_with_identifiers()` from cmd/sign/participant/round2.rs.
|
|
4152
|
+
*/
|
|
4153
|
+
function commitmentsWithIdentifiers$1(commitments, xidToIdentifier) {
|
|
4154
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
4155
|
+
for (const [xidUr, commits] of commitments) {
|
|
4156
|
+
const identifier = xidToIdentifier.get(xidUr);
|
|
4157
|
+
if (!identifier) throw new Error(`Unknown participant ${xidUr}`);
|
|
4158
|
+
mapped.set(identifier, commits);
|
|
4159
|
+
}
|
|
4160
|
+
return mapped;
|
|
4161
|
+
}
|
|
4162
|
+
/**
|
|
4163
|
+
* Build the signRound2Response body envelope.
|
|
4164
|
+
*
|
|
4165
|
+
* Port of response body construction from cmd/sign/participant/round2.rs.
|
|
4166
|
+
*/
|
|
4167
|
+
function buildResponseBody(sessionId, signatureShare, finalizeArid) {
|
|
4168
|
+
const shareHex = serializeSignatureShare(signatureShare);
|
|
4169
|
+
const shareJson = JSON$1.fromString(JSON.stringify({ share: shareHex }));
|
|
4170
|
+
return Envelope.unit().addType("signRound2Response").addAssertion("session", sessionId).addAssertion("signature_share", shareJson).addAssertion("response_arid", finalizeArid);
|
|
4171
|
+
}
|
|
4172
|
+
/**
|
|
4173
|
+
* Persist share state to share.json.
|
|
4174
|
+
*
|
|
4175
|
+
* Port of `persist_share_state()` from cmd/sign/participant/round2.rs.
|
|
4176
|
+
*/
|
|
4177
|
+
function persistShareState(registryPath, groupId, sessionId, responseArid, finalizeArid, signatureShare, commitments) {
|
|
4178
|
+
const dir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
|
|
4179
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4180
|
+
const commitmentsJson = {};
|
|
4181
|
+
for (const [xidUr, commits] of commitments) commitmentsJson[xidUr] = serializeSigningCommitments(commits);
|
|
4182
|
+
const root = {
|
|
4183
|
+
session: sessionId.urString(),
|
|
4184
|
+
response_arid: responseArid.urString(),
|
|
4185
|
+
finalize_arid: finalizeArid.urString(),
|
|
4186
|
+
signature_share: { share: serializeSignatureShare(signatureShare) },
|
|
4187
|
+
commitments: commitmentsJson
|
|
4188
|
+
};
|
|
4189
|
+
fs.writeFileSync(path.join(dir, "share.json"), JSON.stringify(root, null, 2));
|
|
4190
|
+
}
|
|
4191
|
+
/**
|
|
4192
|
+
* Execute the sign participant round 2 command.
|
|
4193
|
+
*
|
|
4194
|
+
* Receives round 2 request and sends signature share.
|
|
4195
|
+
*
|
|
4196
|
+
* Port of `CommandArgs::exec()` from cmd/sign/participant/round2.rs.
|
|
4197
|
+
*/
|
|
4198
|
+
async function round2$1(client, options, cwd) {
|
|
4199
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
4200
|
+
const registry = Registry.load(registryPath);
|
|
4201
|
+
const owner = registry.owner();
|
|
4202
|
+
if (!owner) throw new Error("Registry owner is required");
|
|
4203
|
+
const ownerXidDocument = owner.xidDocument();
|
|
4204
|
+
const sessionId = parseAridUr(options.sessionId);
|
|
4205
|
+
const receiveState = loadReceiveState$1(registryPath, sessionId, options.groupId ? parseAridUr(options.groupId) : void 0);
|
|
4206
|
+
const groupId = receiveState.groupId;
|
|
4207
|
+
const groupRecord = registry.group(groupId);
|
|
4208
|
+
if (!groupRecord) throw new Error("Group not found in registry");
|
|
4209
|
+
if (groupRecord.minSigners() !== receiveState.minSigners) throw new Error(`Session min_signers ${receiveState.minSigners} does not match registry ${groupRecord.minSigners()}`);
|
|
4210
|
+
const registryParticipants = new Set(groupRecord.participants().map((p) => p.xid().urString()));
|
|
4211
|
+
const sessionParticipants = new Set(receiveState.participants.map((p) => p.urString()));
|
|
4212
|
+
if (registryParticipants.size !== sessionParticipants.size || ![...registryParticipants].every((p) => sessionParticipants.has(p))) throw new Error("Session participants do not match registry group participants");
|
|
4213
|
+
if (!sessionParticipants.has(owner.xid().urString())) throw new Error("This participant is not part of the signing session");
|
|
4214
|
+
const listeningAtArid = groupRecord.listeningAtArid();
|
|
4215
|
+
if (!listeningAtArid) throw new Error("No listening ARID for signRound2. Did you run `frost sign participant commit`?");
|
|
4216
|
+
const commitState = loadCommitState(registryPath, groupId, sessionId);
|
|
4217
|
+
if (commitState.nextShareArid.urString() !== listeningAtArid.urString()) throw new Error(`Listening ARID in registry (${listeningAtArid.urString()}) does not match persisted commit state (${commitState.nextShareArid.urString()})`);
|
|
4218
|
+
if (commitState.targetUr !== receiveState.targetUr) throw new Error("Target envelope in commit state does not match persisted signInvite request");
|
|
4219
|
+
const keyPackagePath = groupRecord.contributions().keyPackage;
|
|
4220
|
+
if (!keyPackagePath) throw new Error("Key package path not found; did you finish DKG?");
|
|
4221
|
+
const keyPackage = deserializeKeyPackage(JSON.parse(fs.readFileSync(keyPackagePath, "utf-8")).key_package);
|
|
4222
|
+
const finalizeArid = ARID.new();
|
|
4223
|
+
const targetDigest = Envelope.fromURString(receiveState.targetUr).subject().digest();
|
|
4224
|
+
if (options.verbose === true) console.error("Fetching signRound2 request from Hubert...");
|
|
4225
|
+
const requestEnvelope = await getWithIndicator(client, listeningAtArid, "signRound2 request", options.timeoutSeconds, options.verbose ?? false);
|
|
4226
|
+
if (!requestEnvelope) throw new Error("signRound2 request not found in Hubert storage");
|
|
4227
|
+
const signerPrivateKeys = ownerXidDocument.inceptionPrivateKeys();
|
|
4228
|
+
if (!signerPrivateKeys) throw new Error("Owner XID document has no private keys");
|
|
4229
|
+
const { SealedRequest: SealedRequestClass } = __require("@bcts/gstp");
|
|
4230
|
+
const now = CborDate.now();
|
|
4231
|
+
const sealedRequest = SealedRequestClass.tryFromEnvelope(requestEnvelope, void 0, now, signerPrivateKeys);
|
|
4232
|
+
const expectedCoordinator = groupRecord.coordinator().xid();
|
|
4233
|
+
validateShareRequest(sealedRequest, sessionId, expectedCoordinator);
|
|
4234
|
+
const responseArid = sealedRequest.extractObjectForParameter("response_arid");
|
|
4235
|
+
const commitmentsByXid = extractCommitments(sealedRequest, receiveState);
|
|
4236
|
+
const myCommitments = commitmentsByXid.get(owner.xid().urString());
|
|
4237
|
+
if (!myCommitments) throw new Error("signRound2 request missing commitments for this participant");
|
|
4238
|
+
const myCommitmentsSerialized = serializeSigningCommitments(myCommitments);
|
|
4239
|
+
const storedCommitmentsSerialized = serializeSigningCommitments(commitState.signingCommitments);
|
|
4240
|
+
if (myCommitmentsSerialized.hiding !== storedCommitmentsSerialized.hiding || myCommitmentsSerialized.binding !== storedCommitmentsSerialized.binding) throw new Error("signRound2 request commitments do not match locally stored commitments");
|
|
4241
|
+
const xidToIdentifier = xidIdentifierMap$1(receiveState.participants);
|
|
4242
|
+
if (!xidToIdentifier.get(owner.xid().urString())) throw new Error("Identifier for participant not found");
|
|
4243
|
+
if (keyPackage.minSigners !== receiveState.minSigners) throw new Error(`Key package min_signers ${keyPackage.minSigners} does not match session ${receiveState.minSigners}`);
|
|
4244
|
+
if (commitmentsByXid.size < receiveState.minSigners) throw new Error(`signRound2 request contained ${commitmentsByXid.size} commitments but requires at least ${receiveState.minSigners} signers`);
|
|
4245
|
+
const signatureShare = signingRound2(createSigningPackage(commitmentsWithIdentifiers$1(commitmentsByXid, xidToIdentifier), targetDigest.data()), commitState.signingNonces, keyPackage);
|
|
4246
|
+
const responseBody = buildResponseBody(sessionId, signatureShare, finalizeArid);
|
|
4247
|
+
const { SealedResponse: SealedResponseClass } = __require("@bcts/gstp");
|
|
4248
|
+
const sealedResponse = SealedResponseClass.newSuccess(sealedRequest.id(), ownerXidDocument).withResult(responseBody);
|
|
4249
|
+
if (options.preview === true) {
|
|
4250
|
+
const unsealed = sealedResponse.toEnvelope(void 0, signerPrivateKeys, void 0);
|
|
4251
|
+
console.log(unsealed.urString());
|
|
4252
|
+
return { listeningArid: finalizeArid.urString() };
|
|
4253
|
+
}
|
|
4254
|
+
let coordinatorDoc;
|
|
4255
|
+
if (expectedCoordinator.urString() === owner.xid().urString()) coordinatorDoc = ownerXidDocument;
|
|
4256
|
+
else {
|
|
4257
|
+
const coordinatorRecord = registry.participant(expectedCoordinator);
|
|
4258
|
+
if (!coordinatorRecord) throw new Error(`Coordinator ${expectedCoordinator.urString()} not found in registry`);
|
|
4259
|
+
coordinatorDoc = coordinatorRecord.xidDocument();
|
|
4260
|
+
}
|
|
4261
|
+
const expiry = CborDate.withDurationFromNow(3600);
|
|
4262
|
+
await putWithIndicator(client, responseArid, sealedResponse.toEnvelope(expiry, signerPrivateKeys, coordinatorDoc), "Signature Share", options.verbose ?? false);
|
|
4263
|
+
persistShareState(registryPath, groupId, sessionId, responseArid, finalizeArid, signatureShare, commitmentsByXid);
|
|
4264
|
+
const groupRecordMutable = registry.group(groupId);
|
|
4265
|
+
if (groupRecordMutable) {
|
|
4266
|
+
groupRecordMutable.setListeningAtArid(finalizeArid);
|
|
4267
|
+
registry.save(registryPath);
|
|
4268
|
+
}
|
|
4269
|
+
if (options.verbose === true) console.error(`Posted signature share to ${responseArid.urString()}`);
|
|
4270
|
+
return { listeningArid: finalizeArid.urString() };
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
//#endregion
|
|
4274
|
+
//#region src/cmd/sign/participant/finalize.ts
|
|
4275
|
+
/**
|
|
4276
|
+
* Sign participant finalize command.
|
|
4277
|
+
*
|
|
4278
|
+
* Port of cmd/sign/participant/finalize.rs from frost-hubert-rust.
|
|
4279
|
+
*
|
|
4280
|
+
* @module
|
|
4281
|
+
*/
|
|
4282
|
+
/**
|
|
4283
|
+
* Load the receive state for a signing session.
|
|
4284
|
+
*
|
|
4285
|
+
* Searches for sign_receive.json in group-state directories.
|
|
4286
|
+
*
|
|
4287
|
+
* Port of `load_receive_state()` from cmd/sign/participant/finalize.rs.
|
|
4288
|
+
*/
|
|
4289
|
+
function loadReceiveState(registryPath, sessionId, groupHint) {
|
|
4290
|
+
const base = path.dirname(registryPath);
|
|
4291
|
+
const groupStateDir = path.join(base, "group-state");
|
|
4292
|
+
const groupDirs = [];
|
|
4293
|
+
if (groupHint !== void 0) groupDirs.push([groupHint, path.join(groupStateDir, groupHint.hex())]);
|
|
4294
|
+
else if (fs.existsSync(groupStateDir)) {
|
|
4295
|
+
for (const entry of fs.readdirSync(groupStateDir, { withFileTypes: true })) if (entry.isDirectory()) {
|
|
4296
|
+
const name = entry.name;
|
|
4297
|
+
if (name.length === 64 && /^[0-9a-f]+$/i.test(name)) {
|
|
4298
|
+
const groupId = ARID.fromHex(name);
|
|
4299
|
+
groupDirs.push([groupId, path.join(groupStateDir, name)]);
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
const candidates = [];
|
|
4304
|
+
for (const [groupId, groupDir] of groupDirs) {
|
|
4305
|
+
const candidate = path.join(groupDir, "signing", sessionId.hex(), "sign_receive.json");
|
|
4306
|
+
if (fs.existsSync(candidate)) candidates.push([groupId, candidate]);
|
|
4307
|
+
}
|
|
4308
|
+
if (candidates.length === 0) throw new Error("No sign_receive.json found for this session; run `frost sign participant receive` first");
|
|
4309
|
+
if (candidates.length > 1) throw new Error("Multiple groups contain this session; use --group to disambiguate");
|
|
4310
|
+
const [groupId, filePath] = candidates[0];
|
|
4311
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
4312
|
+
const getStr = (key) => {
|
|
4313
|
+
const val = raw[key];
|
|
4314
|
+
if (typeof val !== "string") throw new Error(`Missing or invalid ${key} in sign_receive.json`);
|
|
4315
|
+
return val;
|
|
4316
|
+
};
|
|
4317
|
+
const sessionInState = parseAridUr(getStr("session"));
|
|
4318
|
+
if (sessionInState.hex() !== sessionId.hex()) throw new Error(`Session ${sessionInState.urString()} in sign_receive.json does not match requested session ${sessionId.urString()}`);
|
|
4319
|
+
const groupInState = parseAridUr(getStr("group"));
|
|
4320
|
+
if (groupInState.hex() !== groupId.hex()) throw new Error(`Group ${groupInState.urString()} in sign_receive.json does not match directory group ${groupId.urString()}`);
|
|
4321
|
+
const coordinator = XID.fromURString(getStr("coordinator"));
|
|
4322
|
+
const participantsVal = raw["participants"];
|
|
4323
|
+
if (!Array.isArray(participantsVal)) throw new Error("Missing participants in sign_receive.json");
|
|
4324
|
+
const participants = [];
|
|
4325
|
+
for (const entry of participantsVal) {
|
|
4326
|
+
if (typeof entry !== "string") throw new Error("Invalid participant entry in sign_receive.json");
|
|
4327
|
+
participants.push(XID.fromURString(entry));
|
|
4328
|
+
}
|
|
4329
|
+
const minSignersVal = raw["min_signers"];
|
|
4330
|
+
if (typeof minSignersVal !== "number") throw new Error("Missing min_signers in sign_receive.json");
|
|
4331
|
+
const minSigners = minSignersVal;
|
|
4332
|
+
const targetUr = getStr("target");
|
|
4333
|
+
participants.sort((a, b) => a.urString().localeCompare(b.urString()));
|
|
4334
|
+
return {
|
|
4335
|
+
groupId,
|
|
4336
|
+
coordinator,
|
|
4337
|
+
participants,
|
|
4338
|
+
minSigners,
|
|
4339
|
+
targetUr
|
|
4340
|
+
};
|
|
4341
|
+
}
|
|
4342
|
+
/**
|
|
4343
|
+
* Load the share state for a signing session.
|
|
4344
|
+
*
|
|
4345
|
+
* Port of `load_share_state()` from cmd/sign/participant/finalize.rs.
|
|
4346
|
+
*/
|
|
4347
|
+
function loadShareState(registryPath, groupId, sessionId) {
|
|
4348
|
+
const dir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
|
|
4349
|
+
const filePath = path.join(dir, "share.json");
|
|
4350
|
+
if (!fs.existsSync(filePath)) throw new Error(`Signature share state not found at ${filePath}. Run \`frost sign participant share\` first.`);
|
|
4351
|
+
const raw = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
4352
|
+
const getStr = (key) => {
|
|
4353
|
+
const val = raw[key];
|
|
4354
|
+
if (typeof val !== "string") throw new Error(`Missing or invalid ${key} in share.json`);
|
|
4355
|
+
return val;
|
|
4356
|
+
};
|
|
4357
|
+
const sessionInState = parseAridUr(getStr("session"));
|
|
4358
|
+
if (sessionInState.hex() !== sessionId.hex()) throw new Error(`Session ${sessionInState.urString()} in share.json does not match requested session ${sessionId.urString()}`);
|
|
4359
|
+
const finalizeArid = parseAridUr(getStr("finalize_arid"));
|
|
4360
|
+
const signatureShare = deserializeSignatureShare(getStr("signature_share"));
|
|
4361
|
+
const commitmentsVal = raw["commitments"];
|
|
4362
|
+
if (typeof commitmentsVal !== "object" || commitmentsVal === null) throw new Error("Missing commitments map in share.json");
|
|
4363
|
+
const commitments = /* @__PURE__ */ new Map();
|
|
4364
|
+
for (const [xidStr, value] of Object.entries(commitmentsVal)) {
|
|
4365
|
+
const commits = deserializeSigningCommitments(value);
|
|
4366
|
+
commitments.set(xidStr, commits);
|
|
4367
|
+
}
|
|
4368
|
+
return {
|
|
4369
|
+
finalizeArid,
|
|
4370
|
+
signatureShare,
|
|
4371
|
+
commitments
|
|
4372
|
+
};
|
|
4373
|
+
}
|
|
4374
|
+
/**
|
|
4375
|
+
* Validate that session state is consistent with registry and owner.
|
|
4376
|
+
*
|
|
4377
|
+
* Port of `validate_session_state()` from cmd/sign/participant/finalize.rs.
|
|
4378
|
+
*/
|
|
4379
|
+
function validateSessionState(receiveState, groupRecord, owner) {
|
|
4380
|
+
if (receiveState.coordinator.urString() !== groupRecord.coordinator().xid().urString()) throw new Error("Coordinator in session state does not match registry");
|
|
4381
|
+
const ownerXidStr = owner.xid().urString();
|
|
4382
|
+
if (!receiveState.participants.some((p) => p.urString() === ownerXidStr)) throw new Error("This participant is not part of the signing session");
|
|
4383
|
+
if (groupRecord.minSigners() !== receiveState.minSigners) throw new Error(`Session min_signers ${receiveState.minSigners} does not match registry ${groupRecord.minSigners()}`);
|
|
4384
|
+
}
|
|
4385
|
+
/**
|
|
4386
|
+
* Validate share state against receive state and registry.
|
|
4387
|
+
*
|
|
4388
|
+
* Port of `validate_share_state()` from cmd/sign/participant/finalize.rs.
|
|
4389
|
+
*/
|
|
4390
|
+
function validateShareState(shareState, receiveState, groupRecord) {
|
|
4391
|
+
const listeningAtArid = groupRecord.listeningAtArid();
|
|
4392
|
+
if (listeningAtArid === void 0) throw new Error("No listening ARID for signFinalize. Did you run `frost sign participant share`?");
|
|
4393
|
+
if (shareState.finalizeArid.hex() !== listeningAtArid.hex()) throw new Error(`Registry listening ARID (${listeningAtArid.urString()}) does not match persisted finalize ARID (${shareState.finalizeArid.urString()})`);
|
|
4394
|
+
const commitParticipants = new Set(shareState.commitments.keys());
|
|
4395
|
+
const sessionParticipants = new Set(receiveState.participants.map((p) => p.urString()));
|
|
4396
|
+
if (commitParticipants.size !== sessionParticipants.size) throw new Error("Commitments do not match session participants");
|
|
4397
|
+
for (const p of commitParticipants) if (!sessionParticipants.has(p)) throw new Error("Commitments do not match session participants");
|
|
4398
|
+
}
|
|
4399
|
+
/**
|
|
4400
|
+
* Validate the finalize event.
|
|
4401
|
+
*
|
|
4402
|
+
* Port of `validate_finalize_event()` from cmd/sign/participant/finalize.rs.
|
|
4403
|
+
*/
|
|
4404
|
+
function validateFinalizeEvent(sealedEvent, sessionId, groupRecord) {
|
|
4405
|
+
const sessionEnvelope = sealedEvent.content().objectForPredicate("session");
|
|
4406
|
+
if (sessionEnvelope === void 0) throw new Error("Missing session in finalize event");
|
|
4407
|
+
const eventSession = ARID.fromTaggedCbor(sessionEnvelope.subject().tryLeaf());
|
|
4408
|
+
if (eventSession.hex() !== sessionId.hex()) throw new Error(`Event session ${eventSession.urString()} does not match expected ${sessionId.urString()}`);
|
|
4409
|
+
const expectedCoordinator = groupRecord.coordinator().xid();
|
|
4410
|
+
if (sealedEvent.sender().xid().urString() !== expectedCoordinator.urString()) throw new Error(`Unexpected event sender: ${sealedEvent.sender().xid().urString()} (expected coordinator ${expectedCoordinator.urString()})`);
|
|
4411
|
+
}
|
|
4412
|
+
/**
|
|
4413
|
+
* Validate signature shares from the finalize event.
|
|
4414
|
+
*
|
|
4415
|
+
* Port of `validate_signature_shares()` from cmd/sign/participant/finalize.rs.
|
|
4416
|
+
*/
|
|
4417
|
+
function validateSignatureShares(signatureSharesByXid, receiveState, _shareState, owner) {
|
|
4418
|
+
if (signatureSharesByXid.size < receiveState.minSigners) throw new Error(`Finalize package contains ${signatureSharesByXid.size} signature shares but requires at least ${receiveState.minSigners}`);
|
|
4419
|
+
const sharesParticipants = new Set(signatureSharesByXid.keys());
|
|
4420
|
+
const sessionParticipants = new Set(receiveState.participants.map((p) => p.urString()));
|
|
4421
|
+
if (sharesParticipants.size !== sessionParticipants.size) throw new Error("Signature share set does not match session participants");
|
|
4422
|
+
for (const p of sharesParticipants) if (!sessionParticipants.has(p)) throw new Error("Signature share set does not match session participants");
|
|
4423
|
+
const ownerXidStr = owner.xid().urString();
|
|
4424
|
+
if (signatureSharesByXid.get(ownerXidStr) === void 0) throw new Error("Finalize package is missing this participant's signature share");
|
|
4425
|
+
}
|
|
4426
|
+
/**
|
|
4427
|
+
* Fetch and parse the finalize event from storage.
|
|
4428
|
+
*
|
|
4429
|
+
* Port of `fetch_finalize_event()` from cmd/sign/participant/finalize.rs.
|
|
4430
|
+
*/
|
|
4431
|
+
async function fetchFinalizeEvent(client, finalizeArid, timeout, owner) {
|
|
4432
|
+
if (isVerbose()) console.error("Fetching finalize package from Hubert...");
|
|
4433
|
+
const finalizeEnvelope = await getWithIndicator(client, finalizeArid, "Finalize package", timeout, isVerbose());
|
|
4434
|
+
if (finalizeEnvelope === null || finalizeEnvelope === void 0) throw new Error("Finalize package not found in Hubert storage");
|
|
4435
|
+
const signerKeys = owner.xidDocument().inceptionPrivateKeys();
|
|
4436
|
+
if (signerKeys === void 0) throw new Error("Owner XID document has no inception private keys");
|
|
4437
|
+
return SealedEvent.tryFromEnvelope(finalizeEnvelope, void 0, void 0, signerKeys, (env) => {
|
|
4438
|
+
SignFinalizeContent.fromEnvelope(env);
|
|
4439
|
+
return env;
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
/**
|
|
4443
|
+
* Parse signature shares from the finalize event.
|
|
4444
|
+
*
|
|
4445
|
+
* Port of `parse_signature_shares()` from cmd/sign/participant/finalize.rs.
|
|
4446
|
+
*/
|
|
4447
|
+
function parseSignatureShares(event) {
|
|
4448
|
+
const contentEnvelope = event.content();
|
|
4449
|
+
const shares = /* @__PURE__ */ new Map();
|
|
4450
|
+
const entries = contentEnvelope.objectsForPredicate("signature_share");
|
|
4451
|
+
for (const entry of entries) {
|
|
4452
|
+
const xid = XID.fromTaggedCbor(entry.subject().tryLeaf());
|
|
4453
|
+
const shareEnvelope = entry.objectForPredicate("share");
|
|
4454
|
+
if (shareEnvelope === void 0) throw new Error("Missing share in signature_share entry");
|
|
4455
|
+
const share = deserializeSignatureShare(shareEnvelope.extractString());
|
|
4456
|
+
const xidStr = xid.urString();
|
|
4457
|
+
if (shares.has(xidStr)) throw new Error(`Duplicate signature share for participant ${xidStr}`);
|
|
4458
|
+
shares.set(xidStr, share);
|
|
4459
|
+
}
|
|
4460
|
+
if (shares.size === 0) throw new Error("Finalize package contains no signature shares");
|
|
4461
|
+
return shares;
|
|
4462
|
+
}
|
|
4463
|
+
/**
|
|
4464
|
+
* Build a mapping from XID to FROST identifier.
|
|
4465
|
+
*
|
|
4466
|
+
* Port of `xid_identifier_map()` from cmd/sign/participant/finalize.rs.
|
|
4467
|
+
*/
|
|
4468
|
+
function xidIdentifierMap(participants) {
|
|
4469
|
+
const map = /* @__PURE__ */ new Map();
|
|
4470
|
+
for (let i = 0; i < participants.length; i++) {
|
|
4471
|
+
const xid = participants[i];
|
|
4472
|
+
const identifier = identifierFromU16(i + 1);
|
|
4473
|
+
map.set(xid.urString(), identifier);
|
|
4474
|
+
}
|
|
4475
|
+
return map;
|
|
4476
|
+
}
|
|
4477
|
+
/**
|
|
4478
|
+
* Convert commitments from XID-keyed to Identifier-keyed map.
|
|
4479
|
+
*
|
|
4480
|
+
* Port of `commitments_with_identifiers()` from cmd/sign/participant/finalize.rs.
|
|
4481
|
+
*/
|
|
4482
|
+
function commitmentsWithIdentifiers(commitments, xidToIdentifier) {
|
|
4483
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
4484
|
+
for (const [xidStr, commits] of commitments) {
|
|
4485
|
+
const identifier = xidToIdentifier.get(xidStr);
|
|
4486
|
+
if (identifier === void 0) throw new Error(`Unknown participant ${xidStr}`);
|
|
4487
|
+
mapped.set(identifier, commits);
|
|
4488
|
+
}
|
|
4489
|
+
return mapped;
|
|
4490
|
+
}
|
|
4491
|
+
/**
|
|
4492
|
+
* Convert signature shares from XID-keyed to Identifier-keyed map.
|
|
4493
|
+
*
|
|
4494
|
+
* Port of `signature_shares_with_identifiers()` from cmd/sign/participant/finalize.rs.
|
|
4495
|
+
*/
|
|
4496
|
+
function signatureSharesWithIdentifiers(shares, xidToIdentifier) {
|
|
4497
|
+
const mapped = /* @__PURE__ */ new Map();
|
|
4498
|
+
for (const [xidStr, share] of shares) {
|
|
4499
|
+
const identifier = xidToIdentifier.get(xidStr);
|
|
4500
|
+
if (identifier === void 0) throw new Error(`Unknown participant ${xidStr}`);
|
|
4501
|
+
mapped.set(identifier, share);
|
|
4502
|
+
}
|
|
4503
|
+
return mapped;
|
|
4504
|
+
}
|
|
4505
|
+
/**
|
|
4506
|
+
* Load the public key package for a group.
|
|
4507
|
+
*
|
|
4508
|
+
* Port of `load_public_key_package()` from cmd/sign/participant/finalize.rs.
|
|
4509
|
+
*/
|
|
4510
|
+
function loadPublicKeyPackage(registryPath, groupId) {
|
|
4511
|
+
const base = path.dirname(registryPath);
|
|
4512
|
+
const directPath = path.join(base, "group-state", groupId.hex(), "public_key_package.json");
|
|
4513
|
+
if (fs.existsSync(directPath)) {
|
|
4514
|
+
const raw = JSON.parse(fs.readFileSync(directPath, "utf-8"));
|
|
4515
|
+
return {
|
|
4516
|
+
package: deserializePublicKeyPackage(raw),
|
|
4517
|
+
verifyingKeyHex: raw.verifyingKey
|
|
4518
|
+
};
|
|
4519
|
+
}
|
|
4520
|
+
const collectedPath = path.join(base, "group-state", groupId.hex(), "collected_finalize.json");
|
|
4521
|
+
if (fs.existsSync(collectedPath)) {
|
|
4522
|
+
const raw = JSON.parse(fs.readFileSync(collectedPath, "utf-8"));
|
|
4523
|
+
const firstEntry = Object.values(raw)[0];
|
|
4524
|
+
if (firstEntry === void 0) throw new Error("collected_finalize.json is empty");
|
|
4525
|
+
const publicKeyValue = firstEntry["public_key_package"];
|
|
4526
|
+
if (publicKeyValue === void 0) throw new Error("public_key_package missing in collected_finalize.json");
|
|
4527
|
+
return {
|
|
4528
|
+
package: deserializePublicKeyPackage(publicKeyValue),
|
|
4529
|
+
verifyingKeyHex: publicKeyValue.verifyingKey
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
throw new Error(`Public key package not found for group ${groupId.urString()}; run finalize respond/collect first`);
|
|
4533
|
+
}
|
|
4534
|
+
/**
|
|
4535
|
+
* Aggregate signature shares and verify the result.
|
|
4536
|
+
*
|
|
4537
|
+
* Port of `aggregate_and_verify_signature()` from cmd/sign/participant/finalize.rs.
|
|
4538
|
+
*/
|
|
4539
|
+
function aggregateAndVerifySignature(registryPath, groupId, participants, commitments, signatureSharesByXid, targetEnvelope, targetDigest) {
|
|
4540
|
+
const xidToIdentifier = xidIdentifierMap(participants);
|
|
4541
|
+
const signingPackage = createSigningPackage(commitmentsWithIdentifiers(commitments, xidToIdentifier), targetDigest.data());
|
|
4542
|
+
const signatureSharesByIdentifier = signatureSharesWithIdentifiers(signatureSharesByXid, xidToIdentifier);
|
|
4543
|
+
const { package: publicKeyPackage, verifyingKeyHex } = loadPublicKeyPackage(registryPath, groupId);
|
|
4544
|
+
const verifyingKey = signingKeyFromVerifying(hexToBytes$1(verifyingKeyHex));
|
|
4545
|
+
const sigBytes = serializeSignature(aggregateSignatures(signingPackage, signatureSharesByIdentifier, publicKeyPackage));
|
|
4546
|
+
if (sigBytes.length !== 64) throw new Error("Aggregated signature is not 64 bytes");
|
|
4547
|
+
const finalSignature = Signature.ed25519FromData(sigBytes);
|
|
4548
|
+
if (!verifyingKey.verify(finalSignature, targetDigest.data())) throw new Error("Aggregated signature failed verification against target digest");
|
|
4549
|
+
const signedEnvelope = targetEnvelope.addAssertion("signed", finalSignature);
|
|
4550
|
+
signedEnvelope.verifySignatureFrom(verifyingKey);
|
|
4551
|
+
return [
|
|
4552
|
+
finalSignature,
|
|
4553
|
+
signedEnvelope,
|
|
4554
|
+
verifyingKey
|
|
4555
|
+
];
|
|
4556
|
+
}
|
|
4557
|
+
/**
|
|
4558
|
+
* Update the registry verifying key if needed.
|
|
4559
|
+
*
|
|
4560
|
+
* Port of `update_registry_verifying_key()` from cmd/sign/participant/finalize.rs.
|
|
4561
|
+
*/
|
|
4562
|
+
function updateRegistryVerifyingKey(registry, registryPath, groupId, verifyingKey, groupRecord) {
|
|
4563
|
+
const existing = groupRecord.verifyingKey();
|
|
4564
|
+
if (existing !== void 0) {
|
|
4565
|
+
if (existing.urString() !== verifyingKey.urString()) throw new Error("Registry verifying key does not match finalize package");
|
|
4566
|
+
} else {
|
|
4567
|
+
const mutableGroup = registry.group(groupId);
|
|
4568
|
+
if (mutableGroup === void 0) throw new Error("Group not found in registry");
|
|
4569
|
+
mutableGroup.setVerifyingKey(verifyingKey);
|
|
4570
|
+
registry.save(registryPath);
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
/**
|
|
4574
|
+
* Persist the final state to disk.
|
|
4575
|
+
*
|
|
4576
|
+
* Port of `persist_final_state()` from cmd/sign/participant/finalize.rs.
|
|
4577
|
+
*/
|
|
4578
|
+
function persistFinalState(registryPath, groupId, sessionId, signature, signedEnvelope, signatureShares, shareState) {
|
|
4579
|
+
const dir = signingStateDir(registryPath, groupId.hex(), sessionId.hex());
|
|
4580
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4581
|
+
const finalPath = path.join(dir, "final.json");
|
|
4582
|
+
let root = {};
|
|
4583
|
+
if (fs.existsSync(finalPath)) root = JSON.parse(fs.readFileSync(finalPath, "utf-8"));
|
|
4584
|
+
const sharesJson = {};
|
|
4585
|
+
for (const [xidStr, share] of signatureShares) sharesJson[xidStr] = serializeSignatureShare(share);
|
|
4586
|
+
const commitmentsJson = {};
|
|
4587
|
+
for (const [xidStr, commits] of shareState.commitments) commitmentsJson[xidStr] = serializeSigningCommitments(commits);
|
|
4588
|
+
root["group"] = groupId.urString();
|
|
4589
|
+
root["session"] = sessionId.urString();
|
|
4590
|
+
root["signature"] = signature.urString();
|
|
4591
|
+
root["signature_shares"] = sharesJson;
|
|
4592
|
+
root["commitments"] = commitmentsJson;
|
|
4593
|
+
root["finalize_arid"] = shareState.finalizeArid.urString();
|
|
4594
|
+
root["signed_target"] = signedEnvelope.urString();
|
|
4595
|
+
fs.writeFileSync(finalPath, JSON.stringify(root, null, 2));
|
|
4596
|
+
}
|
|
4597
|
+
/**
|
|
4598
|
+
* Execute the sign participant finalize command.
|
|
4599
|
+
*
|
|
4600
|
+
* Receives the finalize event with aggregated signature.
|
|
4601
|
+
*
|
|
4602
|
+
* Port of `finalize()` from cmd/sign/participant/finalize.rs.
|
|
4603
|
+
*/
|
|
4604
|
+
async function finalize(client, options, cwd) {
|
|
4605
|
+
const registryPath = resolveRegistryPath(options.registryPath, cwd);
|
|
4606
|
+
const registry = Registry.load(registryPath);
|
|
4607
|
+
const owner = registry.owner();
|
|
4608
|
+
if (owner === void 0) throw new Error("Registry owner is required");
|
|
4609
|
+
const sessionId = parseAridUr(options.sessionId);
|
|
4610
|
+
const receiveState = loadReceiveState(registryPath, sessionId, options.groupId !== void 0 && options.groupId !== "" ? parseAridUr(options.groupId) : void 0);
|
|
4611
|
+
const groupId = receiveState.groupId;
|
|
4612
|
+
const groupRecord = registry.group(groupId);
|
|
4613
|
+
if (groupRecord === void 0) throw new Error("Group not found in registry");
|
|
4614
|
+
validateSessionState(receiveState, groupRecord, owner);
|
|
4615
|
+
const shareState = loadShareState(registryPath, groupId, sessionId);
|
|
4616
|
+
validateShareState(shareState, receiveState, groupRecord);
|
|
4617
|
+
const sealedEvent = await fetchFinalizeEvent(client, shareState.finalizeArid, options.timeoutSeconds, owner);
|
|
4618
|
+
validateFinalizeEvent(sealedEvent, sessionId, groupRecord);
|
|
4619
|
+
const signatureSharesByXid = parseSignatureShares(sealedEvent);
|
|
4620
|
+
validateSignatureShares(signatureSharesByXid, receiveState, shareState, owner);
|
|
4621
|
+
const targetEnvelope = Envelope.fromURString(receiveState.targetUr);
|
|
4622
|
+
const targetDigest = targetEnvelope.subject().digest();
|
|
4623
|
+
const [finalSignature, signedEnvelope, verifyingKey] = aggregateAndVerifySignature(registryPath, groupId, receiveState.participants, shareState.commitments, signatureSharesByXid, targetEnvelope, targetDigest);
|
|
4624
|
+
updateRegistryVerifyingKey(registry, registryPath, groupId, verifyingKey, groupRecord);
|
|
4625
|
+
persistFinalState(registryPath, groupId, sessionId, finalSignature, signedEnvelope, signatureSharesByXid, shareState);
|
|
4626
|
+
const mutableGroupRecord = registry.group(groupId);
|
|
4627
|
+
if (mutableGroupRecord !== void 0) {
|
|
4628
|
+
mutableGroupRecord.clearListeningAtArid();
|
|
4629
|
+
registry.save(registryPath);
|
|
4630
|
+
}
|
|
4631
|
+
const signatureStr = finalSignature.urString();
|
|
4632
|
+
const signedEnvelopeStr = signedEnvelope.urString();
|
|
4633
|
+
if (options.verbose === true) {
|
|
4634
|
+
console.log(signatureStr);
|
|
4635
|
+
console.log(signedEnvelopeStr);
|
|
4636
|
+
}
|
|
4637
|
+
return {
|
|
4638
|
+
signature: signatureStr,
|
|
4639
|
+
signedEnvelope: signedEnvelopeStr
|
|
4640
|
+
};
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
//#endregion
|
|
4644
|
+
//#region src/cmd/sign/participant/index.ts
|
|
4645
|
+
var participant_exports = /* @__PURE__ */ __exportAll({
|
|
4646
|
+
finalize: () => finalize,
|
|
4647
|
+
receive: () => receive,
|
|
4648
|
+
round1: () => round1$1,
|
|
4649
|
+
round2: () => round2$1
|
|
4650
|
+
});
|
|
4651
|
+
|
|
4652
|
+
//#endregion
|
|
4653
|
+
//#region src/cmd/sign/index.ts
|
|
4654
|
+
var sign_exports = /* @__PURE__ */ __exportAll({
|
|
4655
|
+
SignFinalizeContent: () => SignFinalizeContent,
|
|
4656
|
+
coordinator: () => coordinator_exports,
|
|
4657
|
+
participant: () => participant_exports,
|
|
4658
|
+
signingStateDir: () => signingStateDir,
|
|
4659
|
+
signingStateDirForGroup: () => signingStateDirForGroup
|
|
4660
|
+
});
|
|
4661
|
+
|
|
4662
|
+
//#endregion
|
|
4663
|
+
export { setVerbose as C, isVerbose as S, parallelFetchConfigWithTimeout as _, CollectionResult as a, putWithIndicator as b, buildFetchRequests as c, fetchStatusError as d, fetchStatusPending as f, parallelFetch as g, fetchStatusTimeout as h, createStorageClient as i, directionEmoji as l, fetchStatusSuccess as m, dkg_exports as n, DEFAULT_TIMEOUT_SECONDS as o, fetchStatusRejected as p, checkAridExists as r, Direction as s, sign_exports as t, emptyCollectionResult as u, parallelSend as v, groupStateDir as x, getWithIndicator as y };
|
|
4664
|
+
//# sourceMappingURL=cmd-C8pmNd28.mjs.map
|