@bcts/frost-hubert 1.0.0-alpha.23 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/bin/frost.cjs +345 -71
  2. package/dist/bin/frost.cjs.map +1 -1
  3. package/dist/bin/frost.mjs +345 -71
  4. package/dist/bin/frost.mjs.map +1 -1
  5. package/dist/busy-DkM2jAIZ.mjs +27 -0
  6. package/dist/busy-DkM2jAIZ.mjs.map +1 -0
  7. package/dist/busy-EZU7EKr6.cjs +38 -0
  8. package/dist/busy-EZU7EKr6.cjs.map +1 -0
  9. package/dist/cmd/index.cjs +28 -22
  10. package/dist/cmd/index.d.cts +2 -2
  11. package/dist/cmd/index.d.mts +2 -2
  12. package/dist/cmd/index.mjs +7 -3
  13. package/dist/cmd-Bw9_i2_f.cjs +130 -0
  14. package/dist/cmd-Bw9_i2_f.cjs.map +1 -0
  15. package/dist/cmd-CS1uJtuD.mjs +113 -0
  16. package/dist/cmd-CS1uJtuD.mjs.map +1 -0
  17. package/dist/common-CvH6dFvQ.mjs +282 -0
  18. package/dist/common-CvH6dFvQ.mjs.map +1 -0
  19. package/dist/common-DUWvtc08.mjs +96 -0
  20. package/dist/common-DUWvtc08.mjs.map +1 -0
  21. package/dist/common-lKP5EzHy.cjs +372 -0
  22. package/dist/common-lKP5EzHy.cjs.map +1 -0
  23. package/dist/common-lThIvJmZ.cjs +114 -0
  24. package/dist/common-lThIvJmZ.cjs.map +1 -0
  25. package/dist/dkg/index.cjs +6 -102
  26. package/dist/dkg/index.cjs.map +1 -1
  27. package/dist/dkg/index.d.cts +2 -2
  28. package/dist/dkg/index.d.mts +2 -2
  29. package/dist/dkg/index.mjs +4 -101
  30. package/dist/dkg/index.mjs.map +1 -1
  31. package/dist/finalize-BRgJK-Xv.cjs +402 -0
  32. package/dist/finalize-BRgJK-Xv.cjs.map +1 -0
  33. package/dist/finalize-BfLgzn8f.cjs +303 -0
  34. package/dist/finalize-BfLgzn8f.cjs.map +1 -0
  35. package/dist/finalize-CNTDj6aS.mjs +389 -0
  36. package/dist/finalize-CNTDj6aS.mjs.map +1 -0
  37. package/dist/finalize-EC3ikHQq.mjs +252 -0
  38. package/dist/finalize-EC3ikHQq.mjs.map +1 -0
  39. package/dist/finalize-IA01t_Qq.mjs +290 -0
  40. package/dist/finalize-IA01t_Qq.mjs.map +1 -0
  41. package/dist/finalize-UPyI1yb1.cjs +265 -0
  42. package/dist/finalize-UPyI1yb1.cjs.map +1 -0
  43. package/dist/{index-BkqLimZT.d.mts → index-B3c-80VS.d.cts} +26 -3
  44. package/dist/index-B3c-80VS.d.cts.map +1 -0
  45. package/dist/{index-BJlwbPYu.d.cts → index-BgbSGpxn.d.mts} +102 -80
  46. package/dist/index-BgbSGpxn.d.mts.map +1 -0
  47. package/dist/{index-BMbPgH0W.d.cts → index-C8QeHNwa.d.cts} +46 -2
  48. package/dist/{index-BMbPgH0W.d.cts.map → index-C8QeHNwa.d.cts.map} +1 -1
  49. package/dist/{index-DmxfT59Y.d.cts → index-D3QTWkEm.d.mts} +26 -3
  50. package/dist/index-D3QTWkEm.d.mts.map +1 -0
  51. package/dist/{index-DoV5HFvV.d.mts → index-DVbWyOs7.d.mts} +46 -2
  52. package/dist/{index-DoV5HFvV.d.mts.map → index-DVbWyOs7.d.mts.map} +1 -1
  53. package/dist/{index-Dzm1v4_4.d.mts → index-F1iNEAJR.d.cts} +102 -80
  54. package/dist/index-F1iNEAJR.d.cts.map +1 -0
  55. package/dist/index.cjs +31 -23
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.d.cts +4 -4
  58. package/dist/index.d.mts +4 -4
  59. package/dist/index.mjs +9 -4
  60. package/dist/index.mjs.map +1 -1
  61. package/dist/invite-5277FQVT.cjs +274 -0
  62. package/dist/invite-5277FQVT.cjs.map +1 -0
  63. package/dist/invite-DUTcfTgX.cjs +109 -0
  64. package/dist/invite-DUTcfTgX.cjs.map +1 -0
  65. package/dist/invite-IU4n0dq2.mjs +96 -0
  66. package/dist/invite-IU4n0dq2.mjs.map +1 -0
  67. package/dist/invite-RU-OXTNS.mjs +219 -0
  68. package/dist/invite-RU-OXTNS.mjs.map +1 -0
  69. package/dist/parallel-D1R6ZGlY.cjs +318 -0
  70. package/dist/parallel-D1R6ZGlY.cjs.map +1 -0
  71. package/dist/parallel-D6zc6VW4.mjs +235 -0
  72. package/dist/parallel-D6zc6VW4.mjs.map +1 -0
  73. package/dist/proposed-participant-Dm1Eq6mX.cjs +141 -0
  74. package/dist/proposed-participant-Dm1Eq6mX.cjs.map +1 -0
  75. package/dist/proposed-participant-cWM7iUrO.mjs +129 -0
  76. package/dist/proposed-participant-cWM7iUrO.mjs.map +1 -0
  77. package/dist/receive-CAI-x4II.cjs +213 -0
  78. package/dist/receive-CAI-x4II.cjs.map +1 -0
  79. package/dist/receive-D2Nn68L7.mjs +188 -0
  80. package/dist/receive-D2Nn68L7.mjs.map +1 -0
  81. package/dist/receive-DA_KQEgk.mjs +177 -0
  82. package/dist/receive-DA_KQEgk.mjs.map +1 -0
  83. package/dist/receive-kZMsXhbK.cjs +190 -0
  84. package/dist/receive-kZMsXhbK.cjs.map +1 -0
  85. package/dist/registry/index.cjs +85 -10
  86. package/dist/registry/index.cjs.map +1 -1
  87. package/dist/registry/index.d.cts +1 -1
  88. package/dist/registry/index.d.mts +1 -1
  89. package/dist/registry/index.mjs +85 -10
  90. package/dist/registry/index.mjs.map +1 -1
  91. package/dist/{registry-loI1_Mh1.cjs → registry-9puTaRrD.cjs} +1 -1
  92. package/dist/{registry-loI1_Mh1.cjs.map → registry-9puTaRrD.cjs.map} +1 -1
  93. package/dist/{registry-CgrCZ4En.mjs → registry-BpCwtrRt.mjs} +1 -1
  94. package/dist/{registry-CgrCZ4En.mjs.map → registry-BpCwtrRt.mjs.map} +1 -1
  95. package/dist/round1-4Hyx8w0x.cjs +422 -0
  96. package/dist/round1-4Hyx8w0x.cjs.map +1 -0
  97. package/dist/round1-7v9LlE11.mjs +373 -0
  98. package/dist/round1-7v9LlE11.mjs.map +1 -0
  99. package/dist/round1-BHBjru1m.cjs +465 -0
  100. package/dist/round1-BHBjru1m.cjs.map +1 -0
  101. package/dist/round1-CMLKN2RR.mjs +195 -0
  102. package/dist/round1-CMLKN2RR.mjs.map +1 -0
  103. package/dist/round1-CWSXZx5R.cjs +208 -0
  104. package/dist/round1-CWSXZx5R.cjs.map +1 -0
  105. package/dist/round1-CcQCGlIT.mjs +208 -0
  106. package/dist/round1-CcQCGlIT.mjs.map +1 -0
  107. package/dist/round1-Cgm7j1kI.mjs +452 -0
  108. package/dist/round1-Cgm7j1kI.mjs.map +1 -0
  109. package/dist/round1-DQ0fnc1H.cjs +221 -0
  110. package/dist/round1-DQ0fnc1H.cjs.map +1 -0
  111. package/dist/round2-BWz9SQIi.cjs +305 -0
  112. package/dist/round2-BWz9SQIi.cjs.map +1 -0
  113. package/dist/round2-BkNRCXgS.mjs +292 -0
  114. package/dist/round2-BkNRCXgS.mjs.map +1 -0
  115. package/dist/round2-Bl2uK93U.mjs +450 -0
  116. package/dist/round2-Bl2uK93U.mjs.map +1 -0
  117. package/dist/round2-CdUT-AhH.cjs +499 -0
  118. package/dist/round2-CdUT-AhH.cjs.map +1 -0
  119. package/dist/round2-DOA3rnV-.mjs +280 -0
  120. package/dist/round2-DOA3rnV-.mjs.map +1 -0
  121. package/dist/round2-Dg24w-TU.mjs +397 -0
  122. package/dist/round2-Dg24w-TU.mjs.map +1 -0
  123. package/dist/round2-LylCa84n.cjs +293 -0
  124. package/dist/round2-LylCa84n.cjs.map +1 -0
  125. package/dist/round2-o2Q-GMbX.cjs +410 -0
  126. package/dist/round2-o2Q-GMbX.cjs.map +1 -0
  127. package/dist/storage-B-Gu68-O.cjs +79 -0
  128. package/dist/storage-B-Gu68-O.cjs.map +1 -0
  129. package/dist/storage-Bkkliz0K.mjs +74 -0
  130. package/dist/storage-Bkkliz0K.mjs.map +1 -0
  131. package/package.json +10 -10
  132. package/src/bin/frost.ts +849 -128
  133. package/src/cmd/common.ts +19 -1
  134. package/src/cmd/dkg/common.ts +97 -10
  135. package/src/cmd/dkg/coordinator/invite.ts +5 -2
  136. package/src/cmd/dkg/participant/finalize.ts +51 -17
  137. package/src/cmd/dkg/participant/round1.ts +39 -38
  138. package/src/cmd/dkg/participant/round2.ts +60 -26
  139. package/src/cmd/sign/coordinator/round2.ts +5 -1
  140. package/src/cmd/sign/participant/finalize.ts +6 -2
  141. package/src/cmd/sign/participant/receive.ts +5 -2
  142. package/src/dkg/group-invite.ts +12 -2
  143. package/src/dkg/proposed-participant.ts +32 -3
  144. package/src/registry/owner-record.ts +12 -0
  145. package/src/registry/participant-record.ts +35 -2
  146. package/src/registry/registry-impl.ts +74 -18
  147. package/dist/cmd-5yLeC_QL.mjs +0 -4708
  148. package/dist/cmd-5yLeC_QL.mjs.map +0 -1
  149. package/dist/cmd-BfZjC3Uh.cjs +0 -4847
  150. package/dist/cmd-BfZjC3Uh.cjs.map +0 -1
  151. package/dist/index-BJlwbPYu.d.cts.map +0 -1
  152. package/dist/index-BkqLimZT.d.mts.map +0 -1
  153. package/dist/index-DmxfT59Y.d.cts.map +0 -1
  154. package/dist/index-Dzm1v4_4.d.mts.map +0 -1
@@ -355,7 +355,14 @@ export class DkgInvitation {
355
355
  recipientPrivateKeys,
356
356
  );
357
357
 
358
- if (expectedSender !== undefined && sealedRequest.sender().xid() !== expectedSender.xid()) {
358
+ // Rust `group_invite.rs:275-279` uses `!=` on `XID`, which is the
359
+ // derived `PartialEq` (byte-wise compare on the 32-byte ARID). TS
360
+ // `!==` is reference identity — two distinct XID instances with
361
+ // the same bytes would fail. Compare via hex equality instead.
362
+ if (
363
+ expectedSender !== undefined &&
364
+ !sealedRequest.sender().xid().equals(expectedSender.xid())
365
+ ) {
359
366
  throw new Error("Invite sender does not match expected sender");
360
367
  }
361
368
 
@@ -391,7 +398,10 @@ export class DkgInvitation {
391
398
  XIDVerifySignature.Inception,
392
399
  );
393
400
 
394
- if (xidDocument.xid() !== recipientXid) {
401
+ // Rust `group_invite.rs` compares via `XID::PartialEq` (byte-wise).
402
+ // TS reference identity would never match for two distinct
403
+ // `XID` instances representing the same identity.
404
+ if (!xidDocument.xid().equals(recipientXid)) {
395
405
  continue;
396
406
  }
397
407
 
@@ -95,12 +95,41 @@ export class DkgProposedParticipant {
95
95
 
96
96
  /**
97
97
  * Compare participants by XID for sorting.
98
+ *
99
+ * Mirrors Rust `PartialOrd::partial_cmp` which uses
100
+ * `self.xid().cmp(&other.xid())` — i.e. the underlying 32-byte XID
101
+ * data is compared lexicographically (`Vec<u8>` ordering). The
102
+ * earlier port used `xid.toString().localeCompare(...)`, which (a)
103
+ * compares the UR-encoded base32-ish string, not the bytes, and (b)
104
+ * is locale-aware. Sorting on UR strings differs from the byte
105
+ * order whenever the underlying bytes contain values ≥ 0x80, so
106
+ * Rust and TS would assign different FROST identifiers to the same
107
+ * participant set — producing different secret shares.
98
108
  */
99
109
  compareTo(other: DkgProposedParticipant): number {
100
- const thisXid = this.xid().toString();
101
- const otherXid = other.xid().toString();
102
- return thisXid.localeCompare(otherXid);
110
+ return compareXidBytes(this.xid().toData(), other.xid().toData());
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Lexicographic byte compare matching Rust's `Vec<u8>::cmp` /
116
+ * `XID::cmp`. Exported so the cmd-tree call sites (round1 / finalize)
117
+ * can use the same comparator when they sort deduplicated XID lists.
118
+ *
119
+ * `XID` is exactly 32 bytes so this only ever compares two equal-length
120
+ * inputs; the length-tiebreak branch mirrors the generic `Ord` impl on
121
+ * `Vec<u8>` and is included for correctness if ever applied to other
122
+ * byte sequences.
123
+ *
124
+ * @internal
125
+ */
126
+ export function compareXidBytes(a: Uint8Array, b: Uint8Array): number {
127
+ const minLen = Math.min(a.length, b.length);
128
+ for (let i = 0; i < minLen; i++) {
129
+ if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
103
130
  }
131
+ if (a.length !== b.length) return a.length < b.length ? -1 : 1;
132
+ return 0;
104
133
  }
105
134
 
106
135
  /**
@@ -101,8 +101,20 @@ export class OwnerRecord {
101
101
 
102
102
  /**
103
103
  * Deserialize from JSON object.
104
+ *
105
+ * Mirrors Rust's `#[serde(deny_unknown_fields)]` derive on
106
+ * `OwnerRecord` (`owner_record.rs:13-17`) — any field not in
107
+ * `{xid_document, pet_name}` causes Rust's `serde_json::from_str`
108
+ * to error with `unknown field`, and we mirror that here so a
109
+ * registry file produced by a future Rust version with extra
110
+ * fields is rejected explicitly rather than silently lossy.
104
111
  */
105
112
  static fromJSON(json: Record<string, unknown>): OwnerRecord {
113
+ for (const key of Object.keys(json)) {
114
+ if (key !== "xid_document" && key !== "pet_name") {
115
+ throw new Error(`unknown field \`${key}\`, expected \`xid_document\` or \`pet_name\``);
116
+ }
117
+ }
106
118
  const xidDocumentUr = json["xid_document"] as string;
107
119
  const petName = json["pet_name"] as string | undefined;
108
120
  return OwnerRecord.fromSignedXidUr(xidDocumentUr, petName);
@@ -141,8 +141,21 @@ export class ParticipantRecord {
141
141
 
142
142
  /**
143
143
  * Deserialize from JSON object.
144
+ *
145
+ * Mirrors Rust's `#[serde(deny_unknown_fields)]` derive on
146
+ * `ParticipantRecord` (`participant_record.rs:12-17`) — Rust's
147
+ * `serde_json::from_str` errors with `unknown field` for any
148
+ * field outside `{xid_document, pet_name}`. We mirror that
149
+ * behaviour so a registry file produced by a future Rust
150
+ * version with extra fields is rejected explicitly rather than
151
+ * silently lossy.
144
152
  */
145
153
  static fromJSON(json: Record<string, unknown>): ParticipantRecord {
154
+ for (const key of Object.keys(json)) {
155
+ if (key !== "xid_document" && key !== "pet_name") {
156
+ throw new Error(`unknown field \`${key}\`, expected \`xid_document\` or \`pet_name\``);
157
+ }
158
+ }
146
159
  const xidDocumentUr = json["xid_document"] as string;
147
160
  const petName = json["pet_name"] as string | undefined;
148
161
  return ParticipantRecord.recreateFromSerialized(xidDocumentUr, petName);
@@ -167,10 +180,30 @@ function parseSignedXidDocument(xidDocumentUr: string): [string, XIDDocument] {
167
180
  try {
168
181
  envelope = Envelope.fromTaggedCbor(envelopeCbor);
169
182
  } catch {
170
- envelope = Envelope.fromUntaggedCbor(envelopeCbor);
183
+ try {
184
+ envelope = Envelope.fromUntaggedCbor(envelopeCbor);
185
+ } catch (e) {
186
+ throw new Error(
187
+ `Unable to decode XID document envelope: ${(e as Error).message ?? String(e)}`,
188
+ {
189
+ cause: e,
190
+ },
191
+ );
192
+ }
171
193
  }
172
194
 
173
- const document = XIDDocument.fromEnvelope(envelope, undefined, XIDVerifySignature.Inception);
195
+ // Mirror Rust `participant_record.rs:198-203`'s `.context(...)` wrap:
196
+ // any failure from `XIDDocument::from_envelope(..., XIDVerifySignature::Inception)`
197
+ // is surfaced as "XID document must be signed by its inception key: <cause>".
198
+ let document: XIDDocument;
199
+ try {
200
+ document = XIDDocument.fromEnvelope(envelope, undefined, XIDVerifySignature.Inception);
201
+ } catch (e) {
202
+ throw new Error(
203
+ `XID document must be signed by its inception key: ${(e as Error).message ?? String(e)}`,
204
+ { cause: e },
205
+ );
206
+ }
174
207
 
175
208
  return [sanitized, document];
176
209
  }
@@ -13,7 +13,7 @@
13
13
  import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
 
16
- import { type ARID, type XID } from "@bcts/components";
16
+ import { type ARID, type PublicKeys, type XID } from "@bcts/components";
17
17
 
18
18
  import { GroupRecord } from "./group-record.js";
19
19
  import { OwnerRecord } from "./owner-record.js";
@@ -140,20 +140,57 @@ export class Registry {
140
140
  /**
141
141
  * Add a participant.
142
142
  *
143
- * Returns the outcome indicating whether the participant was already present or newly inserted.
143
+ * Mirrors Rust `Registry::add_participant`
144
+ * (`registry_impl.rs:83-124`):
145
+ *
146
+ * 1. If `record.pet_name()` is already used by some other XID,
147
+ * bail with `"Pet name '{name}' already used by another
148
+ * participant"`.
149
+ * 2. If `record.pet_name()` matches an existing record under the
150
+ * *same* XID and the public keys also match, return
151
+ * `AlreadyPresent`.
152
+ * 3. If the pet name matches the same XID but public keys don't,
153
+ * bail with `"Participant already exists with a different pet
154
+ * name"`.
155
+ * 4. Otherwise look up by XID. If present and public-keys + pet-name
156
+ * both match, return `AlreadyPresent`; if XID is present but
157
+ * anything differs, bail. If XID is new, insert and return
158
+ * `Inserted`.
159
+ *
160
+ * The earlier port short-circuited on `participants.has(xidUr)` and
161
+ * always returned `AlreadyPresent` — silently allowing re-adds with
162
+ * a different pet name or different public keys, which Rust
163
+ * correctly forbids.
144
164
  */
145
165
  addParticipant(xid: XID, record: ParticipantRecord): AddOutcome {
146
166
  const xidUr = xid.urString();
167
+ const petName = record.petName();
147
168
 
148
- // Check if already present
149
- if (this._participants.has(xidUr)) {
150
- return AddOutcome.AlreadyPresent;
169
+ // Steps 1–3: pet-name conflict resolution.
170
+ if (petName !== undefined) {
171
+ for (const [existingXidUr, existingRecord] of this._participants) {
172
+ if (existingRecord.petName() === petName) {
173
+ if (existingXidUr !== xidUr) {
174
+ throw new Error(`Pet name '${petName}' already used by another participant`);
175
+ }
176
+ if (publicKeysEqual(existingRecord.publicKeys(), record.publicKeys())) {
177
+ return AddOutcome.AlreadyPresent;
178
+ }
179
+ throw new Error("Participant already exists with a different pet name");
180
+ }
181
+ }
151
182
  }
152
183
 
153
- // Check for conflicting pet name
154
- const petName = record.petName();
155
- if (petName !== undefined && this.petNameExists(petName)) {
156
- throw new Error(`Pet name "${petName}" is already used by another participant`);
184
+ // Step 4: XID lookup.
185
+ const existing = this._participants.get(xidUr);
186
+ if (existing !== undefined) {
187
+ if (
188
+ publicKeysEqual(existing.publicKeys(), record.publicKeys()) &&
189
+ existing.petName() === record.petName()
190
+ ) {
191
+ return AddOutcome.AlreadyPresent;
192
+ }
193
+ throw new Error("Participant already exists with a different pet name");
157
194
  }
158
195
 
159
196
  this._participants.set(xidUr, record);
@@ -287,26 +324,34 @@ export class Registry {
287
324
 
288
325
  /**
289
326
  * Serialize to JSON object.
327
+ *
328
+ * Mirrors Rust `Registry`'s field declaration order
329
+ * (`registry_impl.rs:8-14` — `owner, participants, groups`). JSON
330
+ * object member order is not semantically significant, but
331
+ * `serde_json::to_string_pretty` emits keys in declaration order,
332
+ * so for byte-equal `registry.json` (used by the integration tests
333
+ * as a string assertion) the TS port must match Rust's order.
334
+ * Empty `owner` is omitted via `Option::None` skip in Rust; we
335
+ * reproduce that by only setting the key when the owner exists.
290
336
  */
291
337
  toJSON(): Record<string, unknown> {
338
+ const obj: Record<string, unknown> = {};
339
+
340
+ if (this._owner !== undefined) {
341
+ obj["owner"] = this._owner.toJSON();
342
+ }
343
+
292
344
  const participants: Record<string, unknown> = {};
293
345
  for (const [xidUr, record] of this._participants) {
294
346
  participants[xidUr] = record.toJSON();
295
347
  }
348
+ obj["participants"] = participants;
296
349
 
297
350
  const groups: Record<string, unknown> = {};
298
351
  for (const [aridHex, record] of this._groups) {
299
352
  groups[aridHex] = record.toJSON();
300
353
  }
301
-
302
- const obj: Record<string, unknown> = {
303
- groups,
304
- participants,
305
- };
306
-
307
- if (this._owner !== undefined) {
308
- obj["owner"] = this._owner.toJSON();
309
- }
354
+ obj["groups"] = groups;
310
355
 
311
356
  return obj;
312
357
  }
@@ -366,3 +411,14 @@ export function resolveRegistryPath(registryArg: string | undefined, cwd: string
366
411
  // Otherwise, treat as relative path
367
412
  return path.resolve(cwd, registryArg);
368
413
  }
414
+
415
+ /**
416
+ * Structural equality on `PublicKeys`, mirroring Rust's
417
+ * `PartialEq::eq` derive (per-component comparison of the signing
418
+ * and encapsulation public keys).
419
+ *
420
+ * @internal
421
+ */
422
+ function publicKeysEqual(a: PublicKeys, b: PublicKeys): boolean {
423
+ return a.equals(b);
424
+ }