@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.
- package/dist/bin/frost.cjs +347 -75
- package/dist/bin/frost.cjs.map +1 -1
- package/dist/bin/frost.mjs +347 -75
- package/dist/bin/frost.mjs.map +1 -1
- package/dist/busy-DkM2jAIZ.mjs +27 -0
- package/dist/busy-DkM2jAIZ.mjs.map +1 -0
- package/dist/busy-EZU7EKr6.cjs +38 -0
- package/dist/busy-EZU7EKr6.cjs.map +1 -0
- package/dist/{chunk-uaV2rQ02.cjs → chunk-CZWwpsFl.cjs} +22 -32
- package/dist/{chunk-ClPoSABd.mjs → chunk-CjcI7cDX.mjs} +6 -12
- package/dist/cmd/index.cjs +46 -43
- package/dist/cmd/index.d.cts +2 -4
- package/dist/cmd/index.d.mts +2 -4
- package/dist/cmd/index.mjs +7 -6
- package/dist/cmd-Bw9_i2_f.cjs +130 -0
- package/dist/cmd-Bw9_i2_f.cjs.map +1 -0
- package/dist/cmd-CS1uJtuD.mjs +113 -0
- package/dist/cmd-CS1uJtuD.mjs.map +1 -0
- package/dist/common-CvH6dFvQ.mjs +282 -0
- package/dist/common-CvH6dFvQ.mjs.map +1 -0
- package/dist/common-DUWvtc08.mjs +96 -0
- package/dist/common-DUWvtc08.mjs.map +1 -0
- package/dist/common-lKP5EzHy.cjs +372 -0
- package/dist/common-lKP5EzHy.cjs.map +1 -0
- package/dist/common-lThIvJmZ.cjs +114 -0
- package/dist/common-lThIvJmZ.cjs.map +1 -0
- package/dist/dkg/index.cjs +245 -7
- package/dist/dkg/index.cjs.map +1 -0
- package/dist/dkg/index.d.cts +2 -2
- package/dist/dkg/index.d.mts +2 -2
- package/dist/dkg/index.mjs +238 -2
- package/dist/dkg/index.mjs.map +1 -0
- package/dist/finalize-BRgJK-Xv.cjs +402 -0
- package/dist/finalize-BRgJK-Xv.cjs.map +1 -0
- package/dist/finalize-BfLgzn8f.cjs +303 -0
- package/dist/finalize-BfLgzn8f.cjs.map +1 -0
- package/dist/finalize-CNTDj6aS.mjs +389 -0
- package/dist/finalize-CNTDj6aS.mjs.map +1 -0
- package/dist/finalize-EC3ikHQq.mjs +252 -0
- package/dist/finalize-EC3ikHQq.mjs.map +1 -0
- package/dist/finalize-IA01t_Qq.mjs +290 -0
- package/dist/finalize-IA01t_Qq.mjs.map +1 -0
- package/dist/finalize-UPyI1yb1.cjs +265 -0
- package/dist/finalize-UPyI1yb1.cjs.map +1 -0
- package/dist/frost/index.cjs +8 -9
- package/dist/frost/index.cjs.map +1 -1
- package/dist/frost/index.mjs +2 -3
- package/dist/frost/index.mjs.map +1 -1
- package/dist/{group-invite-Dz1Jmiky.d.cts → index-B3c-80VS.d.cts} +25 -2
- package/dist/index-B3c-80VS.d.cts.map +1 -0
- package/dist/{index-CcvTi5EA.d.cts → index-BgbSGpxn.d.mts} +102 -80
- package/dist/index-BgbSGpxn.d.mts.map +1 -0
- package/dist/{registry-impl-CE76sTXQ.d.cts → index-C8QeHNwa.d.cts} +46 -2
- package/dist/index-C8QeHNwa.d.cts.map +1 -0
- package/dist/{group-invite-Wk9CIbHL.d.mts → index-D3QTWkEm.d.mts} +25 -2
- package/dist/index-D3QTWkEm.d.mts.map +1 -0
- package/dist/{registry-impl-BETn_lEO.d.mts → index-DVbWyOs7.d.mts} +46 -2
- package/dist/index-DVbWyOs7.d.mts.map +1 -0
- package/dist/{index-DNCPeLNM.d.mts → index-F1iNEAJR.d.cts} +102 -80
- package/dist/index-F1iNEAJR.d.cts.map +1 -0
- package/dist/index.cjs +72 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +4 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +11 -10
- package/dist/index.mjs.map +1 -1
- package/dist/invite-5277FQVT.cjs +274 -0
- package/dist/invite-5277FQVT.cjs.map +1 -0
- package/dist/invite-DUTcfTgX.cjs +109 -0
- package/dist/invite-DUTcfTgX.cjs.map +1 -0
- package/dist/invite-IU4n0dq2.mjs +96 -0
- package/dist/invite-IU4n0dq2.mjs.map +1 -0
- package/dist/invite-RU-OXTNS.mjs +219 -0
- package/dist/invite-RU-OXTNS.mjs.map +1 -0
- package/dist/parallel-D1R6ZGlY.cjs +318 -0
- package/dist/parallel-D1R6ZGlY.cjs.map +1 -0
- package/dist/parallel-D6zc6VW4.mjs +235 -0
- package/dist/parallel-D6zc6VW4.mjs.map +1 -0
- package/dist/proposed-participant-Dm1Eq6mX.cjs +141 -0
- package/dist/proposed-participant-Dm1Eq6mX.cjs.map +1 -0
- package/dist/proposed-participant-cWM7iUrO.mjs +129 -0
- package/dist/proposed-participant-cWM7iUrO.mjs.map +1 -0
- package/dist/receive-CAI-x4II.cjs +213 -0
- package/dist/receive-CAI-x4II.cjs.map +1 -0
- package/dist/receive-D2Nn68L7.mjs +188 -0
- package/dist/receive-D2Nn68L7.mjs.map +1 -0
- package/dist/receive-DA_KQEgk.mjs +177 -0
- package/dist/receive-DA_KQEgk.mjs.map +1 -0
- package/dist/receive-kZMsXhbK.cjs +190 -0
- package/dist/receive-kZMsXhbK.cjs.map +1 -0
- package/dist/registry/index.cjs +881 -13
- package/dist/registry/index.cjs.map +1 -0
- package/dist/registry/index.d.cts +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +867 -2
- package/dist/registry/index.mjs.map +1 -0
- package/dist/{registry-FMU-ec5K.cjs → registry-9puTaRrD.cjs} +28 -31
- package/dist/registry-9puTaRrD.cjs.map +1 -0
- package/dist/{registry-BDnNV1Rk.mjs → registry-BpCwtrRt.mjs} +7 -10
- package/dist/{registry-BDnNV1Rk.mjs.map → registry-BpCwtrRt.mjs.map} +1 -1
- package/dist/round1-4Hyx8w0x.cjs +422 -0
- package/dist/round1-4Hyx8w0x.cjs.map +1 -0
- package/dist/round1-7v9LlE11.mjs +373 -0
- package/dist/round1-7v9LlE11.mjs.map +1 -0
- package/dist/round1-BHBjru1m.cjs +465 -0
- package/dist/round1-BHBjru1m.cjs.map +1 -0
- package/dist/round1-CMLKN2RR.mjs +195 -0
- package/dist/round1-CMLKN2RR.mjs.map +1 -0
- package/dist/round1-CWSXZx5R.cjs +208 -0
- package/dist/round1-CWSXZx5R.cjs.map +1 -0
- package/dist/round1-CcQCGlIT.mjs +208 -0
- package/dist/round1-CcQCGlIT.mjs.map +1 -0
- package/dist/round1-Cgm7j1kI.mjs +452 -0
- package/dist/round1-Cgm7j1kI.mjs.map +1 -0
- package/dist/round1-DQ0fnc1H.cjs +221 -0
- package/dist/round1-DQ0fnc1H.cjs.map +1 -0
- package/dist/round2-BWz9SQIi.cjs +305 -0
- package/dist/round2-BWz9SQIi.cjs.map +1 -0
- package/dist/round2-BkNRCXgS.mjs +292 -0
- package/dist/round2-BkNRCXgS.mjs.map +1 -0
- package/dist/round2-Bl2uK93U.mjs +450 -0
- package/dist/round2-Bl2uK93U.mjs.map +1 -0
- package/dist/round2-CdUT-AhH.cjs +499 -0
- package/dist/round2-CdUT-AhH.cjs.map +1 -0
- package/dist/round2-DOA3rnV-.mjs +280 -0
- package/dist/round2-DOA3rnV-.mjs.map +1 -0
- package/dist/round2-Dg24w-TU.mjs +397 -0
- package/dist/round2-Dg24w-TU.mjs.map +1 -0
- package/dist/round2-LylCa84n.cjs +293 -0
- package/dist/round2-LylCa84n.cjs.map +1 -0
- package/dist/round2-o2Q-GMbX.cjs +410 -0
- package/dist/round2-o2Q-GMbX.cjs.map +1 -0
- package/dist/storage-B-Gu68-O.cjs +79 -0
- package/dist/storage-B-Gu68-O.cjs.map +1 -0
- package/dist/storage-Bkkliz0K.mjs +74 -0
- package/dist/storage-Bkkliz0K.mjs.map +1 -0
- package/package.json +17 -17
- package/src/bin/frost.ts +849 -128
- package/src/cmd/common.ts +19 -1
- package/src/cmd/dkg/common.ts +97 -10
- package/src/cmd/dkg/coordinator/invite.ts +5 -2
- package/src/cmd/dkg/participant/finalize.ts +52 -18
- package/src/cmd/dkg/participant/round1.ts +39 -38
- package/src/cmd/dkg/participant/round2.ts +60 -26
- package/src/cmd/sign/coordinator/round2.ts +5 -1
- package/src/cmd/sign/participant/finalize.ts +6 -2
- package/src/cmd/sign/participant/receive.ts +5 -2
- package/src/dkg/group-invite.ts +12 -2
- package/src/dkg/proposed-participant.ts +33 -5
- package/src/frost/index.ts +1 -1
- package/src/registry/owner-record.ts +13 -2
- package/src/registry/participant-record.ts +36 -4
- package/src/registry/registry-impl.ts +74 -18
- package/dist/group-invite-CrbOabFL.cjs +0 -368
- package/dist/group-invite-CrbOabFL.cjs.map +0 -1
- package/dist/group-invite-Dz1Jmiky.d.cts.map +0 -1
- package/dist/group-invite-RPElq-fm.mjs +0 -338
- package/dist/group-invite-RPElq-fm.mjs.map +0 -1
- package/dist/group-invite-Wk9CIbHL.d.mts.map +0 -1
- package/dist/index-CcvTi5EA.d.cts.map +0 -1
- package/dist/index-DNCPeLNM.d.mts.map +0 -1
- package/dist/registry-FMU-ec5K.cjs.map +0 -1
- package/dist/registry-impl-BETn_lEO.d.mts.map +0 -1
- package/dist/registry-impl-C7w4awTv.cjs +0 -865
- package/dist/registry-impl-C7w4awTv.cjs.map +0 -1
- package/dist/registry-impl-CE76sTXQ.d.cts.map +0 -1
- package/dist/registry-impl-eYXVSPwM.mjs +0 -797
- package/dist/registry-impl-eYXVSPwM.mjs.map +0 -1
- package/dist/sign-2bOp18Fs.cjs +0 -4875
- package/dist/sign-2bOp18Fs.cjs.map +0 -1
- package/dist/sign-D8C3HJ4B.mjs +0 -4736
- 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
|
*
|
package/src/cmd/dkg/common.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
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
|
|
94
|
+
* Resolve the registry owner's XID document.
|
|
68
95
|
*
|
|
69
|
-
*
|
|
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
|
|
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
|
|
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().
|
|
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,
|
|
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
|
-
|
|
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:
|
|
90
|
-
commitment:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
//
|
|
99
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
115
|
-
secretJson.
|
|
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
|
|
206
|
-
|
|
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
|
|
405
|
-
|
|
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
|
|
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
|
-
//
|
|
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 =
|
|
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
|
|
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().
|
|
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
|
-
|
|
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
|
|
213
|
-
|
|
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
|
|
301
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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:
|
|
366
|
-
commitment:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
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
|
-
|
|
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
|
|
195
|
-
|
|
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
|
-
|
|
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
|
|