@unlink-xyz/multisig 0.1.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/browser/index.js +29418 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/src/frost/account.d.ts +39 -0
- package/dist/src/frost/account.d.ts.map +1 -0
- package/dist/src/frost/account.js +156 -0
- package/dist/src/frost/coordinator.d.ts +60 -0
- package/dist/src/frost/coordinator.d.ts.map +1 -0
- package/dist/src/frost/coordinator.js +211 -0
- package/dist/src/frost/crypto.d.ts +48 -0
- package/dist/src/frost/crypto.d.ts.map +1 -0
- package/dist/src/frost/crypto.js +79 -0
- package/dist/src/frost/curve.d.ts +97 -0
- package/dist/src/frost/curve.d.ts.map +1 -0
- package/dist/src/frost/curve.js +143 -0
- package/dist/src/frost/dkg.d.ts +36 -0
- package/dist/src/frost/dkg.d.ts.map +1 -0
- package/dist/src/frost/dkg.js +242 -0
- package/dist/src/frost/index.d.ts +16 -0
- package/dist/src/frost/index.d.ts.map +1 -0
- package/dist/src/frost/index.js +15 -0
- package/dist/src/frost/listener.d.ts +31 -0
- package/dist/src/frost/listener.d.ts.map +1 -0
- package/dist/src/frost/listener.js +65 -0
- package/dist/src/frost/mock-coordinator.d.ts +11 -0
- package/dist/src/frost/mock-coordinator.d.ts.map +1 -0
- package/dist/src/frost/mock-coordinator.js +288 -0
- package/dist/src/frost/serialization.d.ts +86 -0
- package/dist/src/frost/serialization.d.ts.map +1 -0
- package/dist/src/frost/serialization.js +193 -0
- package/dist/src/frost/signing.d.ts +64 -0
- package/dist/src/frost/signing.d.ts.map +1 -0
- package/dist/src/frost/signing.js +225 -0
- package/dist/src/frost/test-utils.d.ts +2 -0
- package/dist/src/frost/test-utils.d.ts.map +1 -0
- package/dist/src/frost/test-utils.js +1 -0
- package/dist/src/frost/types.d.ts +154 -0
- package/dist/src/frost/types.d.ts.map +1 -0
- package/dist/src/frost/types.js +4 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +8 -0
- package/dist/src/node.d.ts +2 -0
- package/dist/src/node.d.ts.map +1 -0
- package/dist/src/node.js +1 -0
- package/dist/src/wallet/fs-store.d.ts +16 -0
- package/dist/src/wallet/fs-store.d.ts.map +1 -0
- package/dist/src/wallet/fs-store.js +62 -0
- package/dist/src/wallet/index.d.ts +5 -0
- package/dist/src/wallet/index.d.ts.map +1 -0
- package/dist/src/wallet/index.js +2 -0
- package/dist/src/wallet/indexeddb-store.d.ts +12 -0
- package/dist/src/wallet/indexeddb-store.d.ts.map +1 -0
- package/dist/src/wallet/indexeddb-store.js +69 -0
- package/dist/src/wallet/store.d.ts +14 -0
- package/dist/src/wallet/store.d.ts.map +1 -0
- package/dist/src/wallet/store.js +1 -0
- package/dist/src/wallet/types.d.ts +36 -0
- package/dist/src/wallet/types.d.ts.map +1 -0
- package/dist/src/wallet/types.js +1 -0
- package/dist/src/wallet/wallet.d.ts +3 -0
- package/dist/src/wallet/wallet.d.ts.map +1 -0
- package/dist/src/wallet/wallet.js +42 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/tsup.browser.config.d.ts +7 -0
- package/dist/tsup.browser.config.d.ts.map +1 -0
- package/dist/tsup.browser.config.js +34 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +20 -0
- package/package.json +59 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level multisig account API.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates DKG and signing through the coordinator,
|
|
5
|
+
* producing MultisigAccount instances compatible with the
|
|
6
|
+
* existing Account pattern.
|
|
7
|
+
*/
|
|
8
|
+
import { type Signer } from "@unlink-xyz/core";
|
|
9
|
+
import type { FrostSignature, MultisigAccount, ViewingKeyPair } from "./types.js";
|
|
10
|
+
export declare function createMultisig(params: {
|
|
11
|
+
threshold: number;
|
|
12
|
+
totalShares: number;
|
|
13
|
+
gatewayUrl: string;
|
|
14
|
+
viewingKeyPair?: ViewingKeyPair;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
groupId: string;
|
|
17
|
+
complete: () => Promise<MultisigAccount>;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function joinMultisig(params: {
|
|
20
|
+
code: string;
|
|
21
|
+
gatewayUrl: string;
|
|
22
|
+
viewingKeyPair?: ViewingKeyPair;
|
|
23
|
+
}): Promise<MultisigAccount>;
|
|
24
|
+
export declare function signMultisig(params: {
|
|
25
|
+
account: MultisigAccount;
|
|
26
|
+
message: bigint;
|
|
27
|
+
signingSessionCode: string;
|
|
28
|
+
}): Promise<FrostSignature>;
|
|
29
|
+
/**
|
|
30
|
+
* Create a reusable Signer from a multisig account for use with transact().
|
|
31
|
+
*
|
|
32
|
+
* Each sign() call creates a fresh signing session on the coordinator,
|
|
33
|
+
* so the signer can be called multiple times without nonce reuse.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createFrostSigner(params: {
|
|
36
|
+
account: MultisigAccount;
|
|
37
|
+
participants: number[];
|
|
38
|
+
}): Signer;
|
|
39
|
+
//# sourceMappingURL=account.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../../src/frost/account.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAIL,KAAK,MAAM,EACZ,MAAM,kBAAkB,CAAC;AAU1B,OAAO,KAAK,EAIV,cAAc,EACd,eAAe,EACf,cAAc,EACf,MAAM,YAAY,CAAC;AAwEpB,wBAAsB,cAAc,CAAC,MAAM,EAAE;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,eAAe,CAAC,CAAA;CAAE,CAAC,CAwDzE;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,cAAc,CAAC;CACjC,GAAG,OAAO,CAAC,eAAe,CAAC,CAwC3B;AAED,wBAAsB,YAAY,CAAC,MAAM,EAAE;IACzC,OAAO,EAAE,eAAe,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,GAAG,OAAO,CAAC,cAAc,CAAC,CAuC1B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,OAAO,EAAE,eAAe,CAAC;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB,GAAG,MAAM,CAmBT"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level multisig account API.
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates DKG and signing through the coordinator,
|
|
5
|
+
* producing MultisigAccount instances compatible with the
|
|
6
|
+
* existing Account pattern.
|
|
7
|
+
*/
|
|
8
|
+
import { computeMasterPublicKey, computeNullifyingKey, encodeAddress, } from "@unlink-xyz/core";
|
|
9
|
+
import { FrostCoordinator } from "./coordinator.js";
|
|
10
|
+
import { babyjubjub, modMul } from "./curve.js";
|
|
11
|
+
import { dkgFinalize, dkgRound1, dkgRound2 } from "./dkg.js";
|
|
12
|
+
import { aggregateSignatures, generateCommitment, generateSignatureShare, } from "./signing.js";
|
|
13
|
+
function frostUrl(gatewayUrl) {
|
|
14
|
+
return `${gatewayUrl.replace(/\/$/, "")}/frost`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Derive a participant's verification share from coefficient commitments.
|
|
18
|
+
* VS_i = Σ_j Σ_k (C_jk * i^k)
|
|
19
|
+
*/
|
|
20
|
+
function deriveVerificationShare(participantIndex, coefficientCommitments) {
|
|
21
|
+
const idx = BigInt(participantIndex);
|
|
22
|
+
let result = babyjubjub.identity;
|
|
23
|
+
for (const commitments of coefficientCommitments.values()) {
|
|
24
|
+
let power = 1n;
|
|
25
|
+
for (const commitment of commitments) {
|
|
26
|
+
result = babyjubjub.add(result, babyjubjub.scalarMult(commitment, power));
|
|
27
|
+
power = modMul(power, idx, babyjubjub.order);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function buildMultisigAccount(result, viewingKeyPair, groupId, config, participantIndex, gatewayUrl) {
|
|
33
|
+
const nullifyingKey = computeNullifyingKey(viewingKeyPair.privateKey);
|
|
34
|
+
const masterPublicKey = computeMasterPublicKey(result.groupPublicKey, nullifyingKey);
|
|
35
|
+
const address = encodeAddress({
|
|
36
|
+
masterPublicKey,
|
|
37
|
+
viewingPublicKey: viewingKeyPair.pubkey,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
groupId,
|
|
41
|
+
config,
|
|
42
|
+
participantIndex,
|
|
43
|
+
keyShare: result.keyShare,
|
|
44
|
+
groupPublicKey: result.groupPublicKey,
|
|
45
|
+
coefficientCommitments: result.coefficientCommitments,
|
|
46
|
+
viewingKeyPair,
|
|
47
|
+
nullifyingKey,
|
|
48
|
+
masterPublicKey,
|
|
49
|
+
gatewayUrl,
|
|
50
|
+
address,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Throw if more than one participant distributed a viewing key.
|
|
55
|
+
* Multiple providers would cause participants to disagree on the key.
|
|
56
|
+
*/
|
|
57
|
+
function assertSingleViewingKeyProvider(allRound2) {
|
|
58
|
+
const providers = allRound2.filter((pkg) => pkg.encryptedViewingKeys?.length);
|
|
59
|
+
if (providers.length > 1) {
|
|
60
|
+
throw new Error(`Multiple participants (${providers.map((p) => p.participantIndex).join(", ")}) provided viewing keys — only one is allowed`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function createMultisig(params) {
|
|
64
|
+
if (params.threshold > params.totalShares) {
|
|
65
|
+
throw new Error("threshold must not exceed totalShares");
|
|
66
|
+
}
|
|
67
|
+
const config = {
|
|
68
|
+
threshold: params.threshold,
|
|
69
|
+
totalShares: params.totalShares,
|
|
70
|
+
};
|
|
71
|
+
const coordinator = new FrostCoordinator({
|
|
72
|
+
baseUrl: frostUrl(params.gatewayUrl),
|
|
73
|
+
});
|
|
74
|
+
const { code } = await coordinator.createDkgSession(config.threshold, config.totalShares);
|
|
75
|
+
// Round 1 — generate + submit eagerly so joiners see it
|
|
76
|
+
const { secret, package: round1Pkg } = dkgRound1(config, 1);
|
|
77
|
+
await coordinator.submitRound1(code, round1Pkg);
|
|
78
|
+
return {
|
|
79
|
+
groupId: code,
|
|
80
|
+
complete: async () => {
|
|
81
|
+
const allRound1 = await coordinator.waitForRound1(code);
|
|
82
|
+
const round2Pkg = dkgRound2(secret, allRound1, params.viewingKeyPair);
|
|
83
|
+
await coordinator.submitRound2(code, round2Pkg);
|
|
84
|
+
const allRound2 = await coordinator.waitForRound2(code);
|
|
85
|
+
assertSingleViewingKeyProvider(allRound2);
|
|
86
|
+
const result = dkgFinalize(secret, allRound1, allRound2, params.viewingKeyPair);
|
|
87
|
+
const viewingKeyPair = params.viewingKeyPair ?? result.viewingKeyPair;
|
|
88
|
+
if (!viewingKeyPair) {
|
|
89
|
+
throw new Error("No viewing key available after DKG — at least one participant must provide a viewingKeyPair");
|
|
90
|
+
}
|
|
91
|
+
return buildMultisigAccount(result, viewingKeyPair, code, config, 1, params.gatewayUrl);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export async function joinMultisig(params) {
|
|
96
|
+
const coordinator = new FrostCoordinator({
|
|
97
|
+
baseUrl: frostUrl(params.gatewayUrl),
|
|
98
|
+
});
|
|
99
|
+
const { participantIndex, threshold, totalParticipants } = await coordinator.joinDkgSession(params.code);
|
|
100
|
+
const config = { threshold, totalShares: totalParticipants };
|
|
101
|
+
const { secret, package: round1Pkg } = dkgRound1(config, participantIndex);
|
|
102
|
+
await coordinator.submitRound1(params.code, round1Pkg);
|
|
103
|
+
const allRound1 = await coordinator.waitForRound1(params.code);
|
|
104
|
+
const round2Pkg = dkgRound2(secret, allRound1, params.viewingKeyPair);
|
|
105
|
+
await coordinator.submitRound2(params.code, round2Pkg);
|
|
106
|
+
const allRound2 = await coordinator.waitForRound2(params.code);
|
|
107
|
+
assertSingleViewingKeyProvider(allRound2);
|
|
108
|
+
const result = dkgFinalize(secret, allRound1, allRound2, params.viewingKeyPair);
|
|
109
|
+
const viewingKeyPair = params.viewingKeyPair ?? result.viewingKeyPair;
|
|
110
|
+
if (!viewingKeyPair) {
|
|
111
|
+
throw new Error("No viewing key available after DKG — at least one participant must provide a viewingKeyPair");
|
|
112
|
+
}
|
|
113
|
+
return buildMultisigAccount(result, viewingKeyPair, params.code, config, participantIndex, params.gatewayUrl);
|
|
114
|
+
}
|
|
115
|
+
export async function signMultisig(params) {
|
|
116
|
+
const coordinator = new FrostCoordinator({
|
|
117
|
+
baseUrl: frostUrl(params.account.gatewayUrl),
|
|
118
|
+
});
|
|
119
|
+
const { nonces, commitment } = generateCommitment(params.account.participantIndex);
|
|
120
|
+
await coordinator.submitCommitment(params.signingSessionCode, commitment);
|
|
121
|
+
const allCommitments = await coordinator.waitForCommitments(params.signingSessionCode);
|
|
122
|
+
const share = generateSignatureShare(params.account.keyShare, nonces, params.message, allCommitments, params.account.groupPublicKey);
|
|
123
|
+
await coordinator.submitShare(params.signingSessionCode, share);
|
|
124
|
+
const allShares = await coordinator.waitForShares(params.signingSessionCode);
|
|
125
|
+
// Build verification shares from coefficient commitments for per-share verification
|
|
126
|
+
const verificationShares = new Map();
|
|
127
|
+
for (const s of allShares) {
|
|
128
|
+
verificationShares.set(s.participantIndex, deriveVerificationShare(s.participantIndex, params.account.coefficientCommitments));
|
|
129
|
+
}
|
|
130
|
+
return aggregateSignatures(allCommitments, allShares, params.message, {
|
|
131
|
+
verificationShares,
|
|
132
|
+
groupPublicKey: params.account.groupPublicKey,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Create a reusable Signer from a multisig account for use with transact().
|
|
137
|
+
*
|
|
138
|
+
* Each sign() call creates a fresh signing session on the coordinator,
|
|
139
|
+
* so the signer can be called multiple times without nonce reuse.
|
|
140
|
+
*/
|
|
141
|
+
export function createFrostSigner(params) {
|
|
142
|
+
return {
|
|
143
|
+
publicKey: params.account.groupPublicKey,
|
|
144
|
+
sign: async (message) => {
|
|
145
|
+
const coordinator = new FrostCoordinator({
|
|
146
|
+
baseUrl: frostUrl(params.account.gatewayUrl),
|
|
147
|
+
});
|
|
148
|
+
const { code } = await coordinator.createSigningSession(params.participants, message, params.account.groupId);
|
|
149
|
+
return signMultisig({
|
|
150
|
+
account: params.account,
|
|
151
|
+
message,
|
|
152
|
+
signingSessionCode: code,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FROST coordinator HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the frost service API with polling logic,
|
|
5
|
+
* serialization, and timeout handling.
|
|
6
|
+
*/
|
|
7
|
+
import type { DkgRound1Package, DkgRound2Package, SignatureShare, SigningCommitment } from "./types.js";
|
|
8
|
+
export interface CoordinatorConfig {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
/** Polling interval in ms (default 1000) */
|
|
11
|
+
pollIntervalMs?: number;
|
|
12
|
+
/** Timeout in ms (default 60000) */
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class FrostCoordinator {
|
|
16
|
+
private baseUrl;
|
|
17
|
+
private pollIntervalMs;
|
|
18
|
+
private timeoutMs;
|
|
19
|
+
constructor(config: CoordinatorConfig);
|
|
20
|
+
createDkgSession(threshold: number, totalParticipants: number): Promise<{
|
|
21
|
+
code: string;
|
|
22
|
+
}>;
|
|
23
|
+
joinDkgSession(code: string): Promise<{
|
|
24
|
+
participantIndex: number;
|
|
25
|
+
threshold: number;
|
|
26
|
+
totalParticipants: number;
|
|
27
|
+
}>;
|
|
28
|
+
submitRound1(code: string, pkg: DkgRound1Package): Promise<void>;
|
|
29
|
+
waitForRound1(code: string): Promise<DkgRound1Package[]>;
|
|
30
|
+
submitRound2(code: string, pkg: DkgRound2Package): Promise<void>;
|
|
31
|
+
waitForRound2(code: string): Promise<DkgRound2Package[]>;
|
|
32
|
+
createSigningSession(participants: number[], message: bigint, groupId?: string): Promise<{
|
|
33
|
+
code: string;
|
|
34
|
+
}>;
|
|
35
|
+
submitCommitment(code: string, c: SigningCommitment): Promise<void>;
|
|
36
|
+
waitForCommitments(code: string): Promise<SigningCommitment[]>;
|
|
37
|
+
submitShare(code: string, share: SignatureShare): Promise<void>;
|
|
38
|
+
waitForShares(code: string): Promise<SignatureShare[]>;
|
|
39
|
+
getSigningSessionStatus(code: string): Promise<{
|
|
40
|
+
message: bigint;
|
|
41
|
+
status: string;
|
|
42
|
+
}>;
|
|
43
|
+
getSigningSession(code: string): Promise<SigningSessionInfo>;
|
|
44
|
+
listSigningSessions(groupId: string): Promise<SigningSessionInfo[]>;
|
|
45
|
+
waitForSigningSession(groupId: string, signal?: AbortSignal): Promise<SigningSessionInfo>;
|
|
46
|
+
private pollRound1;
|
|
47
|
+
private pollRound2;
|
|
48
|
+
private pollCommitments;
|
|
49
|
+
private pollShares;
|
|
50
|
+
private pollUntilComplete;
|
|
51
|
+
private toError;
|
|
52
|
+
}
|
|
53
|
+
export type SigningSessionInfo = {
|
|
54
|
+
code: string;
|
|
55
|
+
message: bigint;
|
|
56
|
+
participants: number[];
|
|
57
|
+
status: string;
|
|
58
|
+
groupId?: string;
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=coordinator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coordinator.d.ts","sourceRoot":"","sources":["../../../src/frost/coordinator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAcH,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oCAAoC;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAS;gBAEd,MAAM,EAAE,iBAAiB;IAQ/B,gBAAgB,CACpB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,GACxB,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IActB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAC1C,gBAAgB,EAAE,MAAM,CAAC;QACzB,SAAS,EAAE,MAAM,CAAC;QAClB,iBAAiB,EAAE,MAAM,CAAC;KAC3B,CAAC;IAiBI,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAYhE,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAOxD,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAYhE,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IASxD,oBAAoB,CACxB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAkBtB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAenE,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAO9D,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/D,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAOtD,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAOzC,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAO5D,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,EAAE,CAAC;IASnE,qBAAqB,CACzB,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,kBAAkB,CAAC;YAiBhB,UAAU;YAQV,UAAU;YAQV,eAAe;YAUf,UAAU;YAQV,iBAAiB;YAejB,OAAO;CAItB;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC"}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FROST coordinator HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the frost service API with polling logic,
|
|
5
|
+
* serialization, and timeout handling.
|
|
6
|
+
*/
|
|
7
|
+
import { deserializeBigint, deserializeDkgRound1Package, deserializeDkgRound2Package, deserializeSignatureShare, deserializeSigningCommitment, serializeBigint, serializeDkgRound1Package, serializeDkgRound2Package, serializeSignatureShare, serializeSigningCommitment, } from "./serialization.js";
|
|
8
|
+
export class FrostCoordinator {
|
|
9
|
+
baseUrl;
|
|
10
|
+
pollIntervalMs;
|
|
11
|
+
timeoutMs;
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
14
|
+
this.pollIntervalMs = config.pollIntervalMs ?? 1000;
|
|
15
|
+
this.timeoutMs = config.timeoutMs ?? 120000;
|
|
16
|
+
}
|
|
17
|
+
// === DKG ===
|
|
18
|
+
async createDkgSession(threshold, totalParticipants) {
|
|
19
|
+
const resp = await fetch(`${this.baseUrl}/dkg/groups`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/json" },
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
threshold,
|
|
24
|
+
total_participants: totalParticipants,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
if (!resp.ok)
|
|
28
|
+
throw await this.toError(resp);
|
|
29
|
+
const body = (await resp.json());
|
|
30
|
+
return { code: body.code };
|
|
31
|
+
}
|
|
32
|
+
async joinDkgSession(code) {
|
|
33
|
+
const resp = await fetch(`${this.baseUrl}/dkg/groups/${code}/join`, {
|
|
34
|
+
method: "POST",
|
|
35
|
+
});
|
|
36
|
+
if (!resp.ok)
|
|
37
|
+
throw await this.toError(resp);
|
|
38
|
+
const body = (await resp.json());
|
|
39
|
+
return {
|
|
40
|
+
participantIndex: body.participant_index,
|
|
41
|
+
threshold: body.threshold,
|
|
42
|
+
totalParticipants: body.total_participants,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async submitRound1(code, pkg) {
|
|
46
|
+
const resp = await fetch(`${this.baseUrl}/dkg/groups/${code}/round1`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
participant_index: pkg.participantIndex,
|
|
51
|
+
data: serializeDkgRound1Package(pkg),
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
if (!resp.ok)
|
|
55
|
+
throw await this.toError(resp);
|
|
56
|
+
}
|
|
57
|
+
async waitForRound1(code) {
|
|
58
|
+
return this.pollUntilComplete(() => this.pollRound1(code), deserializeDkgRound1Package);
|
|
59
|
+
}
|
|
60
|
+
async submitRound2(code, pkg) {
|
|
61
|
+
const resp = await fetch(`${this.baseUrl}/dkg/groups/${code}/round2`, {
|
|
62
|
+
method: "POST",
|
|
63
|
+
headers: { "Content-Type": "application/json" },
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
participant_index: pkg.participantIndex,
|
|
66
|
+
data: serializeDkgRound2Package(pkg),
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
if (!resp.ok)
|
|
70
|
+
throw await this.toError(resp);
|
|
71
|
+
}
|
|
72
|
+
async waitForRound2(code) {
|
|
73
|
+
return this.pollUntilComplete(() => this.pollRound2(code), deserializeDkgRound2Package);
|
|
74
|
+
}
|
|
75
|
+
// === Signing ===
|
|
76
|
+
async createSigningSession(participants, message, groupId) {
|
|
77
|
+
const body = {
|
|
78
|
+
participants,
|
|
79
|
+
message: serializeBigint(message),
|
|
80
|
+
};
|
|
81
|
+
if (groupId !== undefined) {
|
|
82
|
+
body.group_id = groupId;
|
|
83
|
+
}
|
|
84
|
+
const resp = await fetch(`${this.baseUrl}/sign/sessions`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
});
|
|
89
|
+
if (!resp.ok)
|
|
90
|
+
throw await this.toError(resp);
|
|
91
|
+
const result = (await resp.json());
|
|
92
|
+
return { code: result.code };
|
|
93
|
+
}
|
|
94
|
+
async submitCommitment(code, c) {
|
|
95
|
+
const resp = await fetch(`${this.baseUrl}/sign/sessions/${code}/commitments`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: { "Content-Type": "application/json" },
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
participant_index: c.participantIndex,
|
|
100
|
+
data: serializeSigningCommitment(c),
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
if (!resp.ok)
|
|
104
|
+
throw await this.toError(resp);
|
|
105
|
+
}
|
|
106
|
+
async waitForCommitments(code) {
|
|
107
|
+
return this.pollUntilComplete(() => this.pollCommitments(code), deserializeSigningCommitment);
|
|
108
|
+
}
|
|
109
|
+
async submitShare(code, share) {
|
|
110
|
+
const resp = await fetch(`${this.baseUrl}/sign/sessions/${code}/shares`, {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "Content-Type": "application/json" },
|
|
113
|
+
body: JSON.stringify({
|
|
114
|
+
participant_index: share.participantIndex,
|
|
115
|
+
data: serializeSignatureShare(share),
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
if (!resp.ok)
|
|
119
|
+
throw await this.toError(resp);
|
|
120
|
+
}
|
|
121
|
+
async waitForShares(code) {
|
|
122
|
+
return this.pollUntilComplete(() => this.pollShares(code), deserializeSignatureShare);
|
|
123
|
+
}
|
|
124
|
+
async getSigningSessionStatus(code) {
|
|
125
|
+
const resp = await fetch(`${this.baseUrl}/sign/sessions/${code}/status`);
|
|
126
|
+
if (!resp.ok)
|
|
127
|
+
throw await this.toError(resp);
|
|
128
|
+
const body = (await resp.json());
|
|
129
|
+
return { ...body, message: deserializeBigint(body.message) };
|
|
130
|
+
}
|
|
131
|
+
async getSigningSession(code) {
|
|
132
|
+
const resp = await fetch(`${this.baseUrl}/sign/sessions/${code}`);
|
|
133
|
+
if (!resp.ok)
|
|
134
|
+
throw await this.toError(resp);
|
|
135
|
+
const body = (await resp.json());
|
|
136
|
+
return deserializeSessionInfo(body);
|
|
137
|
+
}
|
|
138
|
+
async listSigningSessions(groupId) {
|
|
139
|
+
const resp = await fetch(`${this.baseUrl}/sign/sessions?group_id=${encodeURIComponent(groupId)}`);
|
|
140
|
+
if (!resp.ok)
|
|
141
|
+
throw await this.toError(resp);
|
|
142
|
+
const body = (await resp.json());
|
|
143
|
+
return body.sessions.map(deserializeSessionInfo);
|
|
144
|
+
}
|
|
145
|
+
async waitForSigningSession(groupId, signal) {
|
|
146
|
+
const deadline = Date.now() + this.timeoutMs;
|
|
147
|
+
while (Date.now() < deadline) {
|
|
148
|
+
if (signal?.aborted) {
|
|
149
|
+
throw new Error("Aborted");
|
|
150
|
+
}
|
|
151
|
+
const sessions = await this.listSigningSessions(groupId);
|
|
152
|
+
if (sessions.length > 0) {
|
|
153
|
+
return sessions[0];
|
|
154
|
+
}
|
|
155
|
+
await sleep(this.pollIntervalMs);
|
|
156
|
+
}
|
|
157
|
+
throw new Error("Coordinator polling timeout");
|
|
158
|
+
}
|
|
159
|
+
// === Internal ===
|
|
160
|
+
async pollRound1(code) {
|
|
161
|
+
const resp = await fetch(`${this.baseUrl}/dkg/groups/${code}/round1`);
|
|
162
|
+
if (!resp.ok)
|
|
163
|
+
throw await this.toError(resp);
|
|
164
|
+
return (await resp.json());
|
|
165
|
+
}
|
|
166
|
+
async pollRound2(code) {
|
|
167
|
+
const resp = await fetch(`${this.baseUrl}/dkg/groups/${code}/round2`);
|
|
168
|
+
if (!resp.ok)
|
|
169
|
+
throw await this.toError(resp);
|
|
170
|
+
return (await resp.json());
|
|
171
|
+
}
|
|
172
|
+
async pollCommitments(code) {
|
|
173
|
+
const resp = await fetch(`${this.baseUrl}/sign/sessions/${code}/commitments`);
|
|
174
|
+
if (!resp.ok)
|
|
175
|
+
throw await this.toError(resp);
|
|
176
|
+
return (await resp.json());
|
|
177
|
+
}
|
|
178
|
+
async pollShares(code) {
|
|
179
|
+
const resp = await fetch(`${this.baseUrl}/sign/sessions/${code}/shares`);
|
|
180
|
+
if (!resp.ok)
|
|
181
|
+
throw await this.toError(resp);
|
|
182
|
+
return (await resp.json());
|
|
183
|
+
}
|
|
184
|
+
async pollUntilComplete(pollFn, deserializeFn) {
|
|
185
|
+
const deadline = Date.now() + this.timeoutMs;
|
|
186
|
+
while (Date.now() < deadline) {
|
|
187
|
+
const result = await pollFn();
|
|
188
|
+
if (result.complete) {
|
|
189
|
+
return result.packages.map((p) => deserializeFn(p));
|
|
190
|
+
}
|
|
191
|
+
await sleep(this.pollIntervalMs);
|
|
192
|
+
}
|
|
193
|
+
throw new Error("Coordinator polling timeout");
|
|
194
|
+
}
|
|
195
|
+
async toError(resp) {
|
|
196
|
+
const body = await resp.text();
|
|
197
|
+
return new Error(`Coordinator error ${resp.status}: ${body}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function deserializeSessionInfo(raw) {
|
|
201
|
+
return {
|
|
202
|
+
code: raw.code,
|
|
203
|
+
message: deserializeBigint(raw.message),
|
|
204
|
+
participants: raw.participants,
|
|
205
|
+
status: raw.status,
|
|
206
|
+
groupId: raw.group_id ?? undefined,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function sleep(ms) {
|
|
210
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
211
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECDH key agreement + authenticated encryption for DKG share transport.
|
|
3
|
+
*
|
|
4
|
+
* ECDH on BabyJubJub: sharedSecret = myPrivate * theirPublicPoint
|
|
5
|
+
* Symmetric key derived via HKDF-SHA256 from shared secret coordinates.
|
|
6
|
+
* Encryption: ChaCha20-Poly1305 (from @noble/ciphers).
|
|
7
|
+
*/
|
|
8
|
+
import { type Point } from "./curve.js";
|
|
9
|
+
/**
|
|
10
|
+
* Convert a bigint to 32-byte big-endian Uint8Array.
|
|
11
|
+
*/
|
|
12
|
+
export declare function bigintToBytes(n: bigint): Uint8Array;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a 32-byte big-endian Uint8Array to bigint.
|
|
15
|
+
*/
|
|
16
|
+
export declare function bytesToBigint(bytes: Uint8Array): bigint;
|
|
17
|
+
/**
|
|
18
|
+
* Compute ECDH shared secret: mySecret * theirPublicKey.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ecdhSharedSecret(mySecret: bigint, theirPublicKey: Point): Point;
|
|
21
|
+
/**
|
|
22
|
+
* Derive a 32-byte symmetric key from an ECDH shared point.
|
|
23
|
+
* Uses HKDF-SHA256 with domain-separated info.
|
|
24
|
+
*/
|
|
25
|
+
export declare function deriveSymmetricKey(sharedPoint: Point): Uint8Array;
|
|
26
|
+
/**
|
|
27
|
+
* Encrypt arbitrary bytes for transport (ChaCha20-Poly1305, random 12-byte nonce).
|
|
28
|
+
*/
|
|
29
|
+
export declare function encryptBytes(plaintext: Uint8Array, symmetricKey: Uint8Array): {
|
|
30
|
+
ciphertext: Uint8Array;
|
|
31
|
+
nonce: Uint8Array;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Decrypt arbitrary bytes from transport.
|
|
35
|
+
*/
|
|
36
|
+
export declare function decryptBytes(ciphertext: Uint8Array, nonce: Uint8Array, symmetricKey: Uint8Array): Uint8Array;
|
|
37
|
+
/**
|
|
38
|
+
* Encrypt a bigint share for transport.
|
|
39
|
+
*/
|
|
40
|
+
export declare function encryptShare(share: bigint, symmetricKey: Uint8Array): {
|
|
41
|
+
ciphertext: Uint8Array;
|
|
42
|
+
nonce: Uint8Array;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Decrypt a share from transport.
|
|
46
|
+
*/
|
|
47
|
+
export declare function decryptShare(ciphertext: Uint8Array, nonce: Uint8Array, symmetricKey: Uint8Array): bigint;
|
|
48
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../../src/frost/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,OAAO,EAAc,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAIpD;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAQnD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAMvD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,KAAK,GACpB,KAAK,CAEP;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,KAAK,GAAG,UAAU,CAKjE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,UAAU,EACrB,YAAY,EAAE,UAAU,GACvB;IAAE,UAAU,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,CAM/C;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,UAAU,EACjB,YAAY,EAAE,UAAU,GACvB,UAAU,CAGZ;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,UAAU,GACvB;IAAE,UAAU,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,UAAU,CAAA;CAAE,CAE/C;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,UAAU,EACtB,KAAK,EAAE,UAAU,EACjB,YAAY,EAAE,UAAU,GACvB,MAAM,CAER"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECDH key agreement + authenticated encryption for DKG share transport.
|
|
3
|
+
*
|
|
4
|
+
* ECDH on BabyJubJub: sharedSecret = myPrivate * theirPublicPoint
|
|
5
|
+
* Symmetric key derived via HKDF-SHA256 from shared secret coordinates.
|
|
6
|
+
* Encryption: ChaCha20-Poly1305 (from @noble/ciphers).
|
|
7
|
+
*/
|
|
8
|
+
import { chacha20poly1305 } from "@noble/ciphers/chacha";
|
|
9
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
10
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
11
|
+
import { babyjubjub } from "./curve.js";
|
|
12
|
+
const DKG_ENCRYPTION_CONTEXT = new TextEncoder().encode("frost-dkg-share-v1");
|
|
13
|
+
/**
|
|
14
|
+
* Convert a bigint to 32-byte big-endian Uint8Array.
|
|
15
|
+
*/
|
|
16
|
+
export function bigintToBytes(n) {
|
|
17
|
+
const bytes = new Uint8Array(32);
|
|
18
|
+
let val = n;
|
|
19
|
+
for (let i = 31; i >= 0; i--) {
|
|
20
|
+
bytes[i] = Number(val & 0xffn);
|
|
21
|
+
val >>= 8n;
|
|
22
|
+
}
|
|
23
|
+
return bytes;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Convert a 32-byte big-endian Uint8Array to bigint.
|
|
27
|
+
*/
|
|
28
|
+
export function bytesToBigint(bytes) {
|
|
29
|
+
let result = 0n;
|
|
30
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
31
|
+
result = (result << 8n) | BigInt(bytes[i]);
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Compute ECDH shared secret: mySecret * theirPublicKey.
|
|
37
|
+
*/
|
|
38
|
+
export function ecdhSharedSecret(mySecret, theirPublicKey) {
|
|
39
|
+
return babyjubjub.scalarMult(theirPublicKey, mySecret);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Derive a 32-byte symmetric key from an ECDH shared point.
|
|
43
|
+
* Uses HKDF-SHA256 with domain-separated info.
|
|
44
|
+
*/
|
|
45
|
+
export function deriveSymmetricKey(sharedPoint) {
|
|
46
|
+
const ikm = new Uint8Array(64);
|
|
47
|
+
ikm.set(bigintToBytes(sharedPoint[0]), 0);
|
|
48
|
+
ikm.set(bigintToBytes(sharedPoint[1]), 32);
|
|
49
|
+
return hkdf(sha256, ikm, undefined, DKG_ENCRYPTION_CONTEXT, 32);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Encrypt arbitrary bytes for transport (ChaCha20-Poly1305, random 12-byte nonce).
|
|
53
|
+
*/
|
|
54
|
+
export function encryptBytes(plaintext, symmetricKey) {
|
|
55
|
+
const nonce = new Uint8Array(12);
|
|
56
|
+
globalThis.crypto.getRandomValues(nonce);
|
|
57
|
+
const cipher = chacha20poly1305(symmetricKey, nonce);
|
|
58
|
+
const ciphertext = cipher.encrypt(plaintext);
|
|
59
|
+
return { ciphertext, nonce };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Decrypt arbitrary bytes from transport.
|
|
63
|
+
*/
|
|
64
|
+
export function decryptBytes(ciphertext, nonce, symmetricKey) {
|
|
65
|
+
const cipher = chacha20poly1305(symmetricKey, nonce);
|
|
66
|
+
return cipher.decrypt(ciphertext);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Encrypt a bigint share for transport.
|
|
70
|
+
*/
|
|
71
|
+
export function encryptShare(share, symmetricKey) {
|
|
72
|
+
return encryptBytes(bigintToBytes(share), symmetricKey);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Decrypt a share from transport.
|
|
76
|
+
*/
|
|
77
|
+
export function decryptShare(ciphertext, nonce, symmetricKey) {
|
|
78
|
+
return bytesToBigint(decryptBytes(ciphertext, nonce, symmetricKey));
|
|
79
|
+
}
|