@bcts/frost-hubert 1.0.0-alpha.22 → 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 (174) hide show
  1. package/dist/bin/frost.cjs +347 -75
  2. package/dist/bin/frost.cjs.map +1 -1
  3. package/dist/bin/frost.mjs +347 -75
  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/{chunk-uaV2rQ02.cjs → chunk-CZWwpsFl.cjs} +22 -32
  10. package/dist/{chunk-ClPoSABd.mjs → chunk-CjcI7cDX.mjs} +6 -12
  11. package/dist/cmd/index.cjs +46 -43
  12. package/dist/cmd/index.d.cts +2 -4
  13. package/dist/cmd/index.d.mts +2 -4
  14. package/dist/cmd/index.mjs +7 -6
  15. package/dist/cmd-Bw9_i2_f.cjs +130 -0
  16. package/dist/cmd-Bw9_i2_f.cjs.map +1 -0
  17. package/dist/cmd-CS1uJtuD.mjs +113 -0
  18. package/dist/cmd-CS1uJtuD.mjs.map +1 -0
  19. package/dist/common-CvH6dFvQ.mjs +282 -0
  20. package/dist/common-CvH6dFvQ.mjs.map +1 -0
  21. package/dist/common-DUWvtc08.mjs +96 -0
  22. package/dist/common-DUWvtc08.mjs.map +1 -0
  23. package/dist/common-lKP5EzHy.cjs +372 -0
  24. package/dist/common-lKP5EzHy.cjs.map +1 -0
  25. package/dist/common-lThIvJmZ.cjs +114 -0
  26. package/dist/common-lThIvJmZ.cjs.map +1 -0
  27. package/dist/dkg/index.cjs +245 -7
  28. package/dist/dkg/index.cjs.map +1 -0
  29. package/dist/dkg/index.d.cts +2 -2
  30. package/dist/dkg/index.d.mts +2 -2
  31. package/dist/dkg/index.mjs +238 -2
  32. package/dist/dkg/index.mjs.map +1 -0
  33. package/dist/finalize-BRgJK-Xv.cjs +402 -0
  34. package/dist/finalize-BRgJK-Xv.cjs.map +1 -0
  35. package/dist/finalize-BfLgzn8f.cjs +303 -0
  36. package/dist/finalize-BfLgzn8f.cjs.map +1 -0
  37. package/dist/finalize-CNTDj6aS.mjs +389 -0
  38. package/dist/finalize-CNTDj6aS.mjs.map +1 -0
  39. package/dist/finalize-EC3ikHQq.mjs +252 -0
  40. package/dist/finalize-EC3ikHQq.mjs.map +1 -0
  41. package/dist/finalize-IA01t_Qq.mjs +290 -0
  42. package/dist/finalize-IA01t_Qq.mjs.map +1 -0
  43. package/dist/finalize-UPyI1yb1.cjs +265 -0
  44. package/dist/finalize-UPyI1yb1.cjs.map +1 -0
  45. package/dist/frost/index.cjs +8 -9
  46. package/dist/frost/index.cjs.map +1 -1
  47. package/dist/frost/index.mjs +2 -3
  48. package/dist/frost/index.mjs.map +1 -1
  49. package/dist/{group-invite-Dz1Jmiky.d.cts → index-B3c-80VS.d.cts} +25 -2
  50. package/dist/index-B3c-80VS.d.cts.map +1 -0
  51. package/dist/{index-CcvTi5EA.d.cts → index-BgbSGpxn.d.mts} +102 -80
  52. package/dist/index-BgbSGpxn.d.mts.map +1 -0
  53. package/dist/{registry-impl-CE76sTXQ.d.cts → index-C8QeHNwa.d.cts} +46 -2
  54. package/dist/index-C8QeHNwa.d.cts.map +1 -0
  55. package/dist/{group-invite-Wk9CIbHL.d.mts → index-D3QTWkEm.d.mts} +25 -2
  56. package/dist/index-D3QTWkEm.d.mts.map +1 -0
  57. package/dist/{registry-impl-BETn_lEO.d.mts → index-DVbWyOs7.d.mts} +46 -2
  58. package/dist/index-DVbWyOs7.d.mts.map +1 -0
  59. package/dist/{index-DNCPeLNM.d.mts → index-F1iNEAJR.d.cts} +102 -80
  60. package/dist/index-F1iNEAJR.d.cts.map +1 -0
  61. package/dist/index.cjs +72 -68
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.d.cts +4 -7
  64. package/dist/index.d.cts.map +1 -1
  65. package/dist/index.d.mts +4 -7
  66. package/dist/index.d.mts.map +1 -1
  67. package/dist/index.mjs +11 -10
  68. package/dist/index.mjs.map +1 -1
  69. package/dist/invite-5277FQVT.cjs +274 -0
  70. package/dist/invite-5277FQVT.cjs.map +1 -0
  71. package/dist/invite-DUTcfTgX.cjs +109 -0
  72. package/dist/invite-DUTcfTgX.cjs.map +1 -0
  73. package/dist/invite-IU4n0dq2.mjs +96 -0
  74. package/dist/invite-IU4n0dq2.mjs.map +1 -0
  75. package/dist/invite-RU-OXTNS.mjs +219 -0
  76. package/dist/invite-RU-OXTNS.mjs.map +1 -0
  77. package/dist/parallel-D1R6ZGlY.cjs +318 -0
  78. package/dist/parallel-D1R6ZGlY.cjs.map +1 -0
  79. package/dist/parallel-D6zc6VW4.mjs +235 -0
  80. package/dist/parallel-D6zc6VW4.mjs.map +1 -0
  81. package/dist/proposed-participant-Dm1Eq6mX.cjs +141 -0
  82. package/dist/proposed-participant-Dm1Eq6mX.cjs.map +1 -0
  83. package/dist/proposed-participant-cWM7iUrO.mjs +129 -0
  84. package/dist/proposed-participant-cWM7iUrO.mjs.map +1 -0
  85. package/dist/receive-CAI-x4II.cjs +213 -0
  86. package/dist/receive-CAI-x4II.cjs.map +1 -0
  87. package/dist/receive-D2Nn68L7.mjs +188 -0
  88. package/dist/receive-D2Nn68L7.mjs.map +1 -0
  89. package/dist/receive-DA_KQEgk.mjs +177 -0
  90. package/dist/receive-DA_KQEgk.mjs.map +1 -0
  91. package/dist/receive-kZMsXhbK.cjs +190 -0
  92. package/dist/receive-kZMsXhbK.cjs.map +1 -0
  93. package/dist/registry/index.cjs +881 -13
  94. package/dist/registry/index.cjs.map +1 -0
  95. package/dist/registry/index.d.cts +1 -1
  96. package/dist/registry/index.d.mts +1 -1
  97. package/dist/registry/index.mjs +867 -2
  98. package/dist/registry/index.mjs.map +1 -0
  99. package/dist/{registry-FMU-ec5K.cjs → registry-9puTaRrD.cjs} +28 -31
  100. package/dist/registry-9puTaRrD.cjs.map +1 -0
  101. package/dist/{registry-BDnNV1Rk.mjs → registry-BpCwtrRt.mjs} +7 -10
  102. package/dist/{registry-BDnNV1Rk.mjs.map → registry-BpCwtrRt.mjs.map} +1 -1
  103. package/dist/round1-4Hyx8w0x.cjs +422 -0
  104. package/dist/round1-4Hyx8w0x.cjs.map +1 -0
  105. package/dist/round1-7v9LlE11.mjs +373 -0
  106. package/dist/round1-7v9LlE11.mjs.map +1 -0
  107. package/dist/round1-BHBjru1m.cjs +465 -0
  108. package/dist/round1-BHBjru1m.cjs.map +1 -0
  109. package/dist/round1-CMLKN2RR.mjs +195 -0
  110. package/dist/round1-CMLKN2RR.mjs.map +1 -0
  111. package/dist/round1-CWSXZx5R.cjs +208 -0
  112. package/dist/round1-CWSXZx5R.cjs.map +1 -0
  113. package/dist/round1-CcQCGlIT.mjs +208 -0
  114. package/dist/round1-CcQCGlIT.mjs.map +1 -0
  115. package/dist/round1-Cgm7j1kI.mjs +452 -0
  116. package/dist/round1-Cgm7j1kI.mjs.map +1 -0
  117. package/dist/round1-DQ0fnc1H.cjs +221 -0
  118. package/dist/round1-DQ0fnc1H.cjs.map +1 -0
  119. package/dist/round2-BWz9SQIi.cjs +305 -0
  120. package/dist/round2-BWz9SQIi.cjs.map +1 -0
  121. package/dist/round2-BkNRCXgS.mjs +292 -0
  122. package/dist/round2-BkNRCXgS.mjs.map +1 -0
  123. package/dist/round2-Bl2uK93U.mjs +450 -0
  124. package/dist/round2-Bl2uK93U.mjs.map +1 -0
  125. package/dist/round2-CdUT-AhH.cjs +499 -0
  126. package/dist/round2-CdUT-AhH.cjs.map +1 -0
  127. package/dist/round2-DOA3rnV-.mjs +280 -0
  128. package/dist/round2-DOA3rnV-.mjs.map +1 -0
  129. package/dist/round2-Dg24w-TU.mjs +397 -0
  130. package/dist/round2-Dg24w-TU.mjs.map +1 -0
  131. package/dist/round2-LylCa84n.cjs +293 -0
  132. package/dist/round2-LylCa84n.cjs.map +1 -0
  133. package/dist/round2-o2Q-GMbX.cjs +410 -0
  134. package/dist/round2-o2Q-GMbX.cjs.map +1 -0
  135. package/dist/storage-B-Gu68-O.cjs +79 -0
  136. package/dist/storage-B-Gu68-O.cjs.map +1 -0
  137. package/dist/storage-Bkkliz0K.mjs +74 -0
  138. package/dist/storage-Bkkliz0K.mjs.map +1 -0
  139. package/package.json +17 -17
  140. package/src/bin/frost.ts +849 -128
  141. package/src/cmd/common.ts +19 -1
  142. package/src/cmd/dkg/common.ts +97 -10
  143. package/src/cmd/dkg/coordinator/invite.ts +5 -2
  144. package/src/cmd/dkg/participant/finalize.ts +52 -18
  145. package/src/cmd/dkg/participant/round1.ts +39 -38
  146. package/src/cmd/dkg/participant/round2.ts +60 -26
  147. package/src/cmd/sign/coordinator/round2.ts +5 -1
  148. package/src/cmd/sign/participant/finalize.ts +6 -2
  149. package/src/cmd/sign/participant/receive.ts +5 -2
  150. package/src/dkg/group-invite.ts +12 -2
  151. package/src/dkg/proposed-participant.ts +33 -5
  152. package/src/frost/index.ts +1 -1
  153. package/src/registry/owner-record.ts +13 -2
  154. package/src/registry/participant-record.ts +36 -4
  155. package/src/registry/registry-impl.ts +74 -18
  156. package/dist/group-invite-CrbOabFL.cjs +0 -368
  157. package/dist/group-invite-CrbOabFL.cjs.map +0 -1
  158. package/dist/group-invite-Dz1Jmiky.d.cts.map +0 -1
  159. package/dist/group-invite-RPElq-fm.mjs +0 -338
  160. package/dist/group-invite-RPElq-fm.mjs.map +0 -1
  161. package/dist/group-invite-Wk9CIbHL.d.mts.map +0 -1
  162. package/dist/index-CcvTi5EA.d.cts.map +0 -1
  163. package/dist/index-DNCPeLNM.d.mts.map +0 -1
  164. package/dist/registry-FMU-ec5K.cjs.map +0 -1
  165. package/dist/registry-impl-BETn_lEO.d.mts.map +0 -1
  166. package/dist/registry-impl-C7w4awTv.cjs +0 -865
  167. package/dist/registry-impl-C7w4awTv.cjs.map +0 -1
  168. package/dist/registry-impl-CE76sTXQ.d.cts.map +0 -1
  169. package/dist/registry-impl-eYXVSPwM.mjs +0 -797
  170. package/dist/registry-impl-eYXVSPwM.mjs.map +0 -1
  171. package/dist/sign-2bOp18Fs.cjs +0 -4875
  172. package/dist/sign-2bOp18Fs.cjs.map +0 -1
  173. package/dist/sign-D8C3HJ4B.mjs +0 -4736
  174. package/dist/sign-D8C3HJ4B.mjs.map +0 -1
package/src/cmd/common.ts CHANGED
@@ -5,13 +5,31 @@
5
5
  *
6
6
  * Common utilities for commands.
7
7
  *
8
- * Port of cmd/common.rs from frost-hubert-rust.
8
+ * Port of `cmd/common.rs` from `frost-hubert-rust`.
9
+ *
10
+ * Rust's `cmd::common` exports four cross-cutting helpers used by
11
+ * both the DKG and signing subcommand trees:
12
+ *
13
+ * - `parse_arid_ur` / `signing_key_from_verifying` — re-exported
14
+ * here so the TS module layout matches Rust. The implementations
15
+ * live alongside their other DKG-specific siblings in
16
+ * `cmd/dkg/common.ts` so callers in either tree can keep using
17
+ * them; this file just surfaces them at the parallel-to-Rust
18
+ * import path (`@bcts/frost-hubert/cmd/common`).
19
+ * - `group_state_dir` and the verbose-flag helpers are TS-native here.
20
+ *
21
+ * `OptionalStorageSelector` is intentionally not ported: it's a
22
+ * `clap`-specific argument struct and the TS port surfaces the same
23
+ * effect via the `StorageSelection` string-literal union — see the
24
+ * `parity audit` for context.
9
25
  *
10
26
  * @module
11
27
  */
12
28
 
13
29
  import * as path from "node:path";
14
30
 
31
+ export { parseAridUr, signingKeyFromVerifying } from "./dkg/common.js";
32
+
15
33
  /**
16
34
  * Get the group state directory for a given registry path and group ID.
17
35
  *
@@ -13,6 +13,7 @@
13
13
  import * as path from "node:path";
14
14
 
15
15
  import { type ARID, type XID } from "@bcts/components";
16
+ import { compareXidBytes } from "../../dkg/proposed-participant.js";
16
17
  import { type Envelope } from "@bcts/envelope";
17
18
  import { UR } from "@bcts/uniform-resources";
18
19
  import { type XIDDocument } from "@bcts/xid";
@@ -30,19 +31,45 @@ export { groupStateDir } from "../common.js";
30
31
  /**
31
32
  * Parse an ARID from a UR string.
32
33
  *
33
- * Port of `parse_arid_ur()` from cmd/dkg/common.rs.
34
+ * Mirrors Rust `parse_arid_ur` (`cmd/common.rs:27-43`):
35
+ *
36
+ * 1. Trim and reject empty input.
37
+ * 2. Parse as a UR; require `ur_type` of `"arid"`.
38
+ * 3. Try `ARID::try_from(cbor)` (the tagged-CBOR form).
39
+ * 4. If that fails, fall back to interpreting the CBOR as a bare
40
+ * byte string and constructing the ARID from those bytes via
41
+ * `ARID::from_data_ref`.
42
+ *
43
+ * The earlier port only accepted the tagged form; this matches Rust
44
+ * by accepting the byte-string fallback too.
34
45
  */
35
46
  export function parseAridUr(urString: string): ARID {
36
- const ur = UR.fromURString(urString.trim());
47
+ const trimmed = urString.trim();
48
+ if (trimmed.length === 0) {
49
+ throw new Error("ARID is required");
50
+ }
51
+ const ur = UR.fromURString(trimmed);
37
52
 
38
53
  if (ur.urTypeStr() !== "arid") {
39
- throw new Error(`Expected ur:arid, found ur:${ur.urTypeStr()}`);
54
+ throw new Error(`Expected a ur:arid, found ur:${ur.urTypeStr()}`);
40
55
  }
41
56
 
42
57
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports, no-undef
43
58
  const { ARID: ARIDClass } = require("@bcts/components");
44
- // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
45
- return ARIDClass.fromCbor(ur.cbor());
59
+ const cbor = ur.cbor();
60
+ try {
61
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
62
+ return ARIDClass.fromTaggedCbor(cbor);
63
+ } catch {
64
+ // Fall back to a bare byte string payload, mirroring Rust's
65
+ // `CBOR::try_into_byte_string(cbor) → ARID::from_data_ref(bytes)`.
66
+ const bytes = (cbor as { asByteString(): Uint8Array | undefined }).asByteString?.();
67
+ if (bytes === undefined) {
68
+ throw new Error("Invalid ARID payload");
69
+ }
70
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
71
+ return ARIDClass.fromData(bytes);
72
+ }
46
73
  }
47
74
 
48
75
  /**
@@ -64,11 +91,15 @@ export function parseEnvelopeUr(urString: string): Envelope {
64
91
  }
65
92
 
66
93
  /**
67
- * Resolve the sender XID document from the registry.
94
+ * Resolve the registry owner's XID document.
68
95
  *
69
- * Port of `resolve_sender()` from cmd/dkg/common.rs.
96
+ * The earlier port called this `resolveSender(registry)`, but its
97
+ * behaviour — "give me the owner" — has nothing in common with Rust's
98
+ * `resolve_sender(registry, input)` (which resolves an arbitrary
99
+ * named sender). This helper is renamed to its actual behaviour;
100
+ * the Rust-equivalent `resolveSender(registry, input)` lives below.
70
101
  */
71
- export function resolveSender(registry: Registry): XIDDocument {
102
+ export function resolveOwnerXidDocument(registry: Registry): XIDDocument {
72
103
  const owner = registry.owner();
73
104
 
74
105
  if (!owner) {
@@ -78,6 +109,59 @@ export function resolveSender(registry: Registry): XIDDocument {
78
109
  return owner.xidDocument();
79
110
  }
80
111
 
112
+ /**
113
+ * Resolve a sender XID document from the registry by UR or pet name.
114
+ *
115
+ * Mirrors Rust `resolve_sender(registry, input)`
116
+ * (`cmd/dkg/common.rs:76-94`):
117
+ *
118
+ * 1. Trim and reject empty input.
119
+ * 2. Try parsing the input as a `ur:xid`. If that succeeds, look it
120
+ * up via `registry.participant(xid)`; if no record is found,
121
+ * error with `Sender with XID {ur} not found`.
122
+ * 3. Otherwise look it up by pet name via
123
+ * `registry.participantByPetName(name)`; if no record is found,
124
+ * error with `Sender with pet name '{name}' not found`.
125
+ *
126
+ * Note Rust does NOT check the owner here — only the participants
127
+ * map. The earlier inline duplicate in `participant/round1.ts` did
128
+ * an extra owner-check fallback which is removed to match Rust.
129
+ */
130
+ export function resolveSender(registry: Registry, input: string): XIDDocument {
131
+ const trimmed = input.trim();
132
+ if (trimmed.length === 0) {
133
+ throw new Error("Sender is required");
134
+ }
135
+
136
+ // Try parsing as an XID UR string first. `XID.fromURString` throws
137
+ // when the input isn't a `ur:xid`; that's the signal to fall back
138
+ // to pet-name lookup.
139
+ let xid: XID | undefined;
140
+ try {
141
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-require-imports, no-undef
142
+ const { XID: XIDClass } = require("@bcts/components");
143
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
144
+ xid = XIDClass.fromURString(trimmed) as XID;
145
+ } catch {
146
+ xid = undefined;
147
+ }
148
+
149
+ if (xid !== undefined) {
150
+ const record = registry.participant(xid);
151
+ if (record !== undefined) {
152
+ return record.xidDocument();
153
+ }
154
+ throw new Error(`Sender with XID ${xid.urString()} not found`);
155
+ }
156
+
157
+ const result = registry.participantByPetName(trimmed);
158
+ if (result !== undefined) {
159
+ const [, record] = result;
160
+ return record.xidDocument();
161
+ }
162
+ throw new Error(`Sender with pet name '${trimmed}' not found`);
163
+ }
164
+
81
165
  // -----------------------------------------------------------------------------
82
166
  // Participant resolution
83
167
  // -----------------------------------------------------------------------------
@@ -232,9 +316,12 @@ export function participantNamesFromRegistry(
232
316
  ownerXid: XID,
233
317
  ownerPetName: string | undefined,
234
318
  ): string[] {
235
- // Sort by XID UR string
319
+ // Sort by XID byte order — mirrors Rust `XID::cmp` (raw 32-byte
320
+ // lex compare). The earlier port used
321
+ // `urString().localeCompare(...)`, which diverges from byte order
322
+ // for bytes ≥ 0x80 and is locale-aware.
236
323
  const sorted = [...participants].sort((a, b) =>
237
- a.xid().urString().localeCompare(b.xid().urString()),
324
+ compareXidBytes(a.xid().toData(), b.xid().toData()),
238
325
  );
239
326
 
240
327
  return sorted.map((document) => {
@@ -25,7 +25,7 @@ import {
25
25
  } from "../../../registry/index.js";
26
26
  import { putWithIndicator } from "../../busy.js";
27
27
  import { type StorageClient } from "../../storage.js";
28
- import { dkgStateDir, resolveParticipants, resolveSender } from "../common.js";
28
+ import { dkgStateDir, resolveOwnerXidDocument, resolveParticipants } from "../common.js";
29
29
 
30
30
  /**
31
31
  * Options for the DKG invite command.
@@ -101,7 +101,10 @@ function buildInvite(
101
101
  }
102
102
 
103
103
  // Get sender (registry owner)
104
- const sender = resolveSender(registry);
104
+ // The coordinator's invite always identifies the registry owner as
105
+ // the sender. (Rust reads `registry.owner()` directly here too —
106
+ // see `cmd/dkg/coordinator/invite.rs:74-77`.)
107
+ const sender = resolveOwnerXidDocument(registry);
105
108
 
106
109
  // Calculate dates
107
110
  const now = new Date();
@@ -14,6 +14,7 @@ import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
 
16
16
  import { ARID, JSON as JSONWrapper, XID } from "@bcts/components";
17
+ import { compareXidBytes } from "../../../dkg/proposed-participant.js";
17
18
  import { CborDate } from "@bcts/dcbor";
18
19
  import { Envelope, Function as EnvelopeFunction } from "@bcts/envelope";
19
20
  import { SealedRequest, SealedResponse } from "@bcts/gstp";
@@ -85,34 +86,61 @@ function loadRound2State(registryPath: string, groupId: ARID): Round2State {
85
86
  throw new Error(`Round 2 secret not found at ${round2SecretPath}. Did you run round2?`);
86
87
  }
87
88
 
89
+ // Mirrors Rust `frost::keys::dkg::round2::SecretPackage` JSON
90
+ // (`frost-rust/frost-core/src/keys/dkg.rs:269-287`):
91
+ //
92
+ // {
93
+ // "identifier": "<lowercase hex scalar>",
94
+ // "commitment": ["<hex>", "<hex>", ...],
95
+ // "secret_share": "<hex>",
96
+ // "min_signers": <u16>,
97
+ // "max_signers": <u16>
98
+ // }
99
+ //
100
+ // The struct is `#[serde(deny_unknown_fields)]` and the
101
+ // `commitment` is a `VerifiableSecretSharingCommitment` (a
102
+ // single-field tuple struct over `Vec<CoefficientCommitment>`),
103
+ // which serde flattens to a bare JSON array. The earlier port
104
+ // emitted camelCase keys plus a nested `commitment.coefficients`
105
+ // shape and a numeric `identifier`, which Rust would reject and
106
+ // which had no chance of being read by Rust's standard derive.
88
107
  const secretJson = JSON.parse(fs.readFileSync(round2SecretPath, "utf-8")) as {
89
- identifier: number;
90
- commitment: {
91
- coefficients: string[];
92
- };
93
- secretShare: string;
94
- minSigners: number;
95
- maxSigners: number;
108
+ identifier: string;
109
+ commitment: string[];
110
+ secret_share: string;
111
+ min_signers: number;
112
+ max_signers: number;
96
113
  };
97
114
 
98
- // Reconstruct the round 2 secret package
99
- const identifier = identifierFromU16(secretJson.identifier);
115
+ // Identifier hex little-endian u16 (the FROST 1-indexed
116
+ // participant position). The scalar bytes are 32-LE for Ed25519, so
117
+ // the first two bytes hold the u16 value when the identifier is in
118
+ // the small-integer range (1..=N) used by the DKG.
119
+ const idBytes = hexToBytes(secretJson.identifier);
120
+ let identifierU16 = 1;
121
+ if (idBytes.length >= 2) {
122
+ identifierU16 = idBytes[0] | (idBytes[1] << 8);
123
+ }
124
+ if (identifierU16 === 0) {
125
+ identifierU16 = 1;
126
+ }
127
+ const identifier = identifierFromU16(identifierU16);
100
128
 
101
- const coefficientCommitments = secretJson.commitment.coefficients.map((hex) =>
129
+ const coefficientCommitments = secretJson.commitment.map((hex) =>
102
130
  CoefficientCommitment.deserialize(Ed25519Sha512, hexToBytes(hex)),
103
131
  );
104
132
 
105
133
  const commitment = new VerifiableSecretSharingCommitment(Ed25519Sha512, coefficientCommitments);
106
134
 
107
- const secretShareScalar = Ed25519Sha512.deserializeScalar(hexToBytes(secretJson.secretShare));
135
+ const secretShareScalar = Ed25519Sha512.deserializeScalar(hexToBytes(secretJson.secret_share));
108
136
 
109
137
  const secretPackage: DkgRound2SecretPackage = new round2.SecretPackage(
110
138
  Ed25519Sha512,
111
139
  identifier,
112
140
  commitment,
113
141
  secretShareScalar,
114
- secretJson.minSigners,
115
- secretJson.maxSigners,
142
+ secretJson.min_signers,
143
+ secretJson.max_signers,
116
144
  );
117
145
 
118
146
  // Load collected Round 1 packages (from round2 phase)
@@ -202,8 +230,11 @@ function extractFinalizePackages(
202
230
  sortedXids.push(ownerXid);
203
231
  }
204
232
 
205
- // Sort by XID UR string
206
- sortedXids.sort((a, b) => a.urString().localeCompare(b.urString()));
233
+ // Sort by XID byte order — mirrors Rust `XID::cmp` (raw 32-byte
234
+ // lex compare). The earlier port used `urString().localeCompare(...)`,
235
+ // which differs from byte order for any byte ≥ 0x80 and is locale-
236
+ // aware, producing different FROST identifier assignments than Rust.
237
+ sortedXids.sort((a, b) => compareXidBytes(a.toData(), b.toData()));
207
238
 
208
239
  // Deduplicate
209
240
  const deduped: XID[] = [];
@@ -401,8 +432,11 @@ export async function finalize(
401
432
  sortedXids.push(owner.xid());
402
433
  }
403
434
 
404
- // Sort by XID UR string
405
- sortedXids.sort((a, b) => a.urString().localeCompare(b.urString()));
435
+ // Sort by XID byte order — mirrors Rust `XID::cmp` (raw 32-byte
436
+ // lex compare). The earlier port used `urString().localeCompare(...)`,
437
+ // which differs from byte order for any byte ≥ 0x80 and is locale-
438
+ // aware, producing different FROST identifier assignments than Rust.
439
+ sortedXids.sort((a, b) => compareXidBytes(a.toData(), b.toData()));
406
440
 
407
441
  // Deduplicate
408
442
  const deduped: XID[] = [];
@@ -447,7 +481,7 @@ export async function finalize(
447
481
  );
448
482
 
449
483
  // Get the group verifying key
450
- const verifyingKeyBytes = publicKeyPackage.verifyingKey as Uint8Array;
484
+ const verifyingKeyBytes = publicKeyPackage.verifyingKey;
451
485
  const groupVerifyingKey = signingKeyFromVerifying(verifyingKeyBytes);
452
486
 
453
487
  if (isVerbose() || options.verbose === true) {
@@ -13,7 +13,8 @@
13
13
  import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
 
16
- import { ARID, JSON as JSONWrapper, XID } from "@bcts/components";
16
+ import { ARID, JSON as JSONWrapper, type XID } from "@bcts/components";
17
+ import { compareXidBytes } from "../../../dkg/proposed-participant.js";
17
18
  import { Envelope } from "@bcts/envelope";
18
19
  import { SealedResponse } from "@bcts/gstp";
19
20
  import type { XIDDocument } from "@bcts/xid";
@@ -32,6 +33,7 @@ import {
32
33
  groupParticipantFromRegistry,
33
34
  parseAridUr,
34
35
  parseEnvelopeUr,
36
+ resolveSender,
35
37
  } from "../common.js";
36
38
  import {
37
39
  dkgPart1,
@@ -154,7 +156,27 @@ function buildResponseBody(
154
156
  * so we manually serialize it here.
155
157
  */
156
158
  function serializeRound1SecretPackage(secret: DkgRound1SecretPackage): Record<string, unknown> {
157
- // Access the coefficients and serialize them
159
+ // Mirrors the on-disk shape produced by Rust
160
+ // `serde_json::to_vec_pretty(&frost::keys::dkg::round1::SecretPackage)`
161
+ // (see `frost-rust/frost-core/src/keys/dkg.rs:120-139`):
162
+ //
163
+ // {
164
+ // "identifier": "<lowercase hex scalar>",
165
+ // "coefficients": ["<hex>", "<hex>", ...],
166
+ // "commitment": ["<hex>", "<hex>", ...],
167
+ // "min_signers": <u16>,
168
+ // "max_signers": <u16>
169
+ // }
170
+ //
171
+ // `frost::keys::dkg::round1::SecretPackage` is `#[serde(deny_unknown_fields)]`
172
+ // and has no `header` field (the secret package is private to the
173
+ // participant). The earlier port emitted a top-level `header` which
174
+ // would fail `deny_unknown_fields` validation if Rust ever loaded
175
+ // the file.
176
+ //
177
+ // `Identifier`/`SerializableScalar` serialize via
178
+ // `serdect::array::serialize_hex_lower_or_bin` → lowercase hex for
179
+ // JSON, which `bytesToHex` produces.
158
180
  const coefficients = secret.coefficients();
159
181
  const serializedCoefficients = coefficients.map((c: unknown) =>
160
182
  bytesToHex(
@@ -162,12 +184,10 @@ function serializeRound1SecretPackage(secret: DkgRound1SecretPackage): Record<st
162
184
  ),
163
185
  );
164
186
 
165
- // Get the commitment coefficients
166
187
  const commitment = secret.commitment;
167
188
  const commitmentCoefficients = commitment.serialize().map((c: Uint8Array) => bytesToHex(c));
168
189
 
169
190
  return {
170
- header: serde.DEFAULT_HEADER,
171
191
  identifier: bytesToHex(secret.identifier.serialize()),
172
192
  coefficients: serializedCoefficients,
173
193
  commitment: commitmentCoefficients,
@@ -237,10 +257,13 @@ export async function round1(
237
257
  throw new Error("Registry owner with private keys is required");
238
258
  }
239
259
 
240
- // Resolve expected sender if provided
260
+ // Resolve expected sender if provided. Uses the shared helper from
261
+ // `cmd/dkg/common.ts` (mirrors Rust `resolve_sender`); the
262
+ // duplicated inline implementation that this previously called has
263
+ // been removed.
241
264
  let expectedSender: XIDDocument | undefined;
242
265
  if (options.sender !== undefined) {
243
- expectedSender = resolveSenderXidDocument(registry, options.sender);
266
+ expectedSender = resolveSender(registry, options.sender);
244
267
  }
245
268
 
246
269
  const nextResponseArid =
@@ -263,9 +286,14 @@ export async function round1(
263
286
  expectedSender,
264
287
  );
265
288
 
266
- // Sort participants by XID and find our position
289
+ // Sort participants by XID byte order (mirrors Rust `XID::cmp` —
290
+ // raw 32-byte lex compare). The earlier port used
291
+ // `xid.urString().localeCompare(...)`, which differs from byte
292
+ // order when bytes ≥ 0x80 are present and is locale-aware,
293
+ // producing different FROST identifier assignments and therefore
294
+ // different secret shares than Rust.
267
295
  const sortedParticipants = [...details.participants].sort((a, b) =>
268
- a.xid().urString().localeCompare(b.xid().urString()),
296
+ compareXidBytes(a.xid().toData(), b.xid().toData()),
269
297
  );
270
298
 
271
299
  const ownerIndex = sortedParticipants.findIndex(
@@ -436,33 +464,6 @@ export async function round1(
436
464
  }
437
465
  }
438
466
 
439
- /**
440
- * Resolve a sender XID document from the registry by UR or pet name.
441
- */
442
- function resolveSenderXidDocument(registry: Registry, raw: string): XIDDocument {
443
- // Try parsing as XID UR first
444
- try {
445
- const xid = XID.fromURString(raw.trim());
446
- const record = registry.participant(xid);
447
- if (record) {
448
- return record.xidDocument();
449
- }
450
- const owner = registry.owner();
451
- if (owner?.xid().urString() === xid.urString()) {
452
- return owner.xidDocument();
453
- }
454
- throw new Error(`Sender with XID ${xid.urString()} not found in registry`);
455
- } catch {
456
- // Try looking up by pet name
457
- const result = registry.participantByPetName(raw.trim());
458
- if (result) {
459
- const [, record] = result;
460
- return record.xidDocument();
461
- }
462
- const owner = registry.owner();
463
- if (owner?.petName() === raw.trim()) {
464
- return owner.xidDocument();
465
- }
466
- throw new Error(`Sender '${raw}' not found in registry`);
467
- }
468
- }
467
+ // `resolveSenderXidDocument` removed — it was an inline duplicate of
468
+ // Rust `resolve_sender(registry, input)`. The shared helper now lives
469
+ // in `cmd/dkg/common.ts` and is imported above.
@@ -14,6 +14,7 @@ import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
 
16
16
  import { ARID, JSON as JSONWrapper, XID } from "@bcts/components";
17
+ import { compareXidBytes } from "../../../dkg/proposed-participant.js";
17
18
  import { CborDate } from "@bcts/dcbor";
18
19
  import { Envelope, Function as EnvelopeFunction } from "@bcts/envelope";
19
20
  import { SealedRequest, SealedResponse } from "@bcts/gstp";
@@ -87,8 +88,12 @@ function loadRound1State(registryPath: string, groupId: ARID): Round1State {
87
88
  );
88
89
  }
89
90
 
91
+ // Mirrors Rust `frost::keys::dkg::round1::SecretPackage` JSON
92
+ // (`frost-rust/frost-core/src/keys/dkg.rs:120-139`): no `header`,
93
+ // snake_case `min_signers` / `max_signers`, identifier and
94
+ // coefficients as lowercase hex strings, commitment as a flat array
95
+ // of hex strings.
90
96
  const secretJson = JSON.parse(fs.readFileSync(round1SecretPath, "utf-8")) as {
91
- header: { version: number; ciphersuite: string };
92
97
  identifier: string;
93
98
  coefficients: string[];
94
99
  commitment: string[];
@@ -209,8 +214,11 @@ function extractRound1Packages(
209
214
  sortedXids.push(ownerXid);
210
215
  }
211
216
 
212
- // Sort by XID UR string
213
- sortedXids.sort((a, b) => a.urString().localeCompare(b.urString()));
217
+ // Sort by XID byte order — mirrors Rust `XID::cmp` (raw 32-byte
218
+ // lex compare). The earlier port used `urString().localeCompare(...)`,
219
+ // which differs from byte order for any byte ≥ 0x80 and is locale-
220
+ // aware, producing different FROST identifier assignments than Rust.
221
+ sortedXids.sort((a, b) => compareXidBytes(a.toData(), b.toData()));
214
222
 
215
223
  // Deduplicate
216
224
  const deduped: XID[] = [];
@@ -297,8 +305,11 @@ function buildResponseBody(
297
305
  sortedXids.push(participantXid);
298
306
  }
299
307
 
300
- // Sort by XID UR string
301
- sortedXids.sort((a, b) => a.urString().localeCompare(b.urString()));
308
+ // Sort by XID byte order — mirrors Rust `XID::cmp` (raw 32-byte
309
+ // lex compare). The earlier port used `urString().localeCompare(...)`,
310
+ // which differs from byte order for any byte ≥ 0x80 and is locale-
311
+ // aware, producing different FROST identifier assignments than Rust.
312
+ sortedXids.sort((a, b) => compareXidBytes(a.toData(), b.toData()));
302
313
 
303
314
  // Deduplicate
304
315
  const deduped: XID[] = [];
@@ -348,27 +359,45 @@ function buildResponseBody(
348
359
  /**
349
360
  * Serialize round 2 secret package to JSON format for persistence.
350
361
  *
351
- * The format matches what finalize.ts expects to deserialize.
362
+ * Mirrors the on-disk shape produced by Rust
363
+ * `serde_json::to_vec_pretty(&frost::keys::dkg::round2::SecretPackage)`
364
+ * (see `frost-rust/frost-core/src/keys/dkg.rs:269-287`):
365
+ *
366
+ * ```json
367
+ * {
368
+ * "identifier": "<lowercase hex scalar>",
369
+ * "commitment": ["<hex>", "<hex>", ...],
370
+ * "secret_share": "<hex>",
371
+ * "min_signers": <u16>,
372
+ * "max_signers": <u16>
373
+ * }
374
+ * ```
375
+ *
376
+ * `frost::keys::dkg::round2::SecretPackage` is
377
+ * `#[serde(deny_unknown_fields)]`. The earlier port emitted
378
+ * camelCase keys (`secretShare`, `minSigners`, `maxSigners`), a
379
+ * nested `commitment.coefficients` shape, and a numeric
380
+ * `identifier` — all of which Rust's standard derive would
381
+ * reject and which had no chance of round-tripping with Rust.
382
+ *
383
+ * `participantIndex` is unused now that the identifier comes
384
+ * directly from `secret.identifier.serialize()`; we keep it in the
385
+ * signature for source-level parity with the call sites.
352
386
  */
353
387
  function serializeRound2SecretPackage(
354
388
  secret: DkgRound2SecretPackage,
355
- participantIndex: number,
389
+ _participantIndex: number,
356
390
  ): Record<string, unknown> {
357
- // Get the commitment coefficients
358
391
  const commitment = secret.commitment;
359
392
  const commitmentCoefficients = commitment.serialize().map((c: Uint8Array) => bytesToHex(c));
360
-
361
- // Serialize the secret share
362
393
  const secretShare = bytesToHex(Ed25519Sha512.serializeScalar(secret.secretShare()));
363
394
 
364
395
  return {
365
- identifier: participantIndex,
366
- commitment: {
367
- coefficients: commitmentCoefficients,
368
- },
369
- secretShare,
370
- minSigners: secret.minSigners,
371
- maxSigners: secret.maxSigners,
396
+ identifier: bytesToHex(secret.identifier.serialize()),
397
+ commitment: commitmentCoefficients,
398
+ secret_share: secretShare,
399
+ min_signers: secret.minSigners,
400
+ max_signers: secret.maxSigners,
372
401
  };
373
402
  }
374
403
 
@@ -576,15 +605,20 @@ export async function round2(
576
605
  };
577
606
  }
578
607
 
579
- // Calculate participant index for serialization
580
- // Sort participants by XID to find our position
581
- const sortedXids = groupRecord.participants().map((p) => p.xid().urString());
582
- const ownerXidStr = owner.xid().urString();
583
- if (!sortedXids.includes(ownerXidStr)) {
584
- sortedXids.push(ownerXidStr);
585
- }
586
- sortedXids.sort();
587
- const participantIndex = sortedXids.indexOf(ownerXidStr) + 1; // 1-indexed
608
+ // Calculate participant index for serialization. Sort by XID byte
609
+ // order (mirrors Rust `XID::cmp`) so the 1-indexed position matches
610
+ // what the coordinator used when assigning FROST identifiers. The
611
+ // earlier port sorted by UR string via `Array.sort()` (default JS
612
+ // string compare), which diverges from Rust byte order for any
613
+ // byte ≥ 0x80.
614
+ const sortedParticipantXids: XID[] = groupRecord.participants().map((p) => p.xid());
615
+ const ownerXid = owner.xid();
616
+ const ownerXidStr = ownerXid.urString();
617
+ if (!sortedParticipantXids.some((x) => x.urString() === ownerXidStr)) {
618
+ sortedParticipantXids.push(ownerXid);
619
+ }
620
+ sortedParticipantXids.sort((a, b) => compareXidBytes(a.toData(), b.toData()));
621
+ const participantIndex = sortedParticipantXids.findIndex((x) => x.urString() === ownerXidStr) + 1; // 1-indexed
588
622
 
589
623
  // Persist Round 2 secret and collected round1 packages
590
624
  const round2SecretPath = persistRound2State(
@@ -25,6 +25,7 @@ import {
25
25
  JSON as JSONComponent,
26
26
  } from "@bcts/components";
27
27
  import { Envelope } from "@bcts/envelope";
28
+ import { compareXidBytes } from "../../../dkg/proposed-participant.js";
28
29
  import { type XIDDocument } from "@bcts/xid";
29
30
 
30
31
  import { Registry, resolveRegistryPath } from "../../../registry/index.js";
@@ -405,7 +406,10 @@ function loadStartState(registryPath: string, sessionId: ARID, groupHint?: ARID)
405
406
  for (const xidStr of Object.keys(participantsVal)) {
406
407
  participants.push(XIDClass.fromURString(xidStr));
407
408
  }
408
- participants.sort((a, b) => a.urString().localeCompare(b.urString()));
409
+ // Sort by XID byte order — mirrors Rust `XID::cmp` (raw 32-byte
410
+ // lex compare). The earlier port used
411
+ // `urString().localeCompare(...)`, which diverges for bytes ≥ 0x80.
412
+ participants.sort((a, b) => compareXidBytes(a.toData(), b.toData()));
409
413
 
410
414
  const targetUr = getStr("target");
411
415
 
@@ -14,6 +14,7 @@ import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
 
16
16
  import { ARID, type Digest, Signature, type SigningPublicKey, XID } from "@bcts/components";
17
+ import { compareXidBytes } from "../../../dkg/proposed-participant.js";
17
18
  import { Envelope } from "@bcts/envelope";
18
19
  import { SealedEvent } from "@bcts/gstp";
19
20
 
@@ -191,8 +192,11 @@ function loadReceiveState(
191
192
 
192
193
  const targetUr = getStr("target");
193
194
 
194
- // Sort participants by XID UR string
195
- participants.sort((a, b) => a.urString().localeCompare(b.urString()));
195
+ // Sort participants by XID byte order — mirrors Rust `XID::cmp`.
196
+ // The earlier port used `urString().localeCompare(...)`, which
197
+ // diverges from byte order for bytes ≥ 0x80 and is locale-aware,
198
+ // producing a different signing-package order than Rust.
199
+ participants.sort((a, b) => compareXidBytes(a.toData(), b.toData()));
196
200
 
197
201
  return {
198
202
  groupId,
@@ -14,6 +14,7 @@ import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
15
 
16
16
  import { type ARID, type XID } from "@bcts/components";
17
+ import { compareXidBytes } from "../../../dkg/proposed-participant.js";
17
18
  import { CborDate } from "@bcts/dcbor";
18
19
  import type { Envelope } from "@bcts/envelope";
19
20
 
@@ -335,8 +336,10 @@ export async function receive(
335
336
  throw new Error("signInvite request missing response ARID");
336
337
  }
337
338
 
338
- // Sort participants by XID
339
- participants.sort((a, b) => a.urString().localeCompare(b.urString()));
339
+ // Sort participants by XID byte order — mirrors Rust `XID::cmp`.
340
+ // The earlier port used `urString().localeCompare(...)`, which
341
+ // diverges from byte order for bytes ≥ 0x80 and is locale-aware.
342
+ participants.sort((a, b) => compareXidBytes(a.toData(), b.toData()));
340
343
 
341
344
  const targetEnvelope = sealedRequest.objectForParameter("target");
342
345