Skip to content

Commit f715e71

Browse files
feat: store and restore ecdsa dkg sessions
Ticket: WP-5188 TICKET: WP-5188
1 parent 6b84534 commit f715e71

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

modules/sdk-lib-mpc/src/tss/ecdsa-dkls/dkg.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ type BundlerWasmer = typeof import('@silencelaboratories/dkls-wasm-ll-bundler');
1010

1111
type DklsWasm = NodeWasmer | WebWasmer | BundlerWasmer;
1212

13+
export interface DkgSessionData {
14+
dkgSessionBytes: Uint8Array;
15+
dkgState: DkgState;
16+
chainCodeCommitment?: Uint8Array;
17+
keyShareBuff?: Buffer;
18+
}
19+
1320
export class Dkg {
1421
protected dkgSession: KeygenSession | undefined;
1522
protected dkgSessionBytes: Uint8Array;
@@ -159,6 +166,7 @@ export class Dkg {
159166
}
160167
try {
161168
const payload = this.dkgSession.createFirstMessage().payload;
169+
this.dkgSessionBytes = this.dkgSession.toBytes();
162170
this._deserializeState();
163171
return {
164172
payload: payload,
@@ -276,4 +284,67 @@ export class Dkg {
276284
}
277285
return nextRoundDeserializedMessages;
278286
}
287+
288+
/**
289+
* Get the current session data that can be used to restore the session later
290+
* @returns The current session data
291+
*/
292+
getSessionData(): DkgSessionData {
293+
const sessionData: DkgSessionData = {
294+
dkgSessionBytes: this.dkgSessionBytes,
295+
dkgState: this.dkgState,
296+
};
297+
298+
if (this.chainCodeCommitment) {
299+
sessionData.chainCodeCommitment = this.chainCodeCommitment;
300+
}
301+
302+
if (this.keyShareBuff) {
303+
sessionData.keyShareBuff = this.keyShareBuff;
304+
}
305+
306+
return sessionData;
307+
}
308+
309+
/**
310+
* Restore a DKG session from previous session data
311+
* Note: This should not be used for Round 1 as that's the initialization phase
312+
* @param n Number of parties
313+
* @param t Threshold
314+
* @param partyIdx Party index
315+
* @param sessionData Previous session data
316+
* @param seed Optional seed
317+
* @param retrofitData Optional retrofit data
318+
* @param dklsWasm Optional DKLS wasm instance
319+
* @returns A new DKG instance with the restored session
320+
*/
321+
static async restoreSession(
322+
n: number,
323+
t: number,
324+
partyIdx: number,
325+
sessionData: DkgSessionData,
326+
seed?: Buffer,
327+
retrofitData?: RetrofitData,
328+
dklsWasm?: BundlerWasmer
329+
): Promise<Dkg> {
330+
const dkg = new Dkg(n, t, partyIdx, seed, retrofitData, dklsWasm);
331+
332+
if (!dkg.dklsWasm) {
333+
await dkg.loadDklsWasm();
334+
}
335+
336+
dkg.dkgSessionBytes = sessionData.dkgSessionBytes;
337+
dkg.dkgState = sessionData.dkgState;
338+
339+
if (sessionData.chainCodeCommitment) {
340+
dkg.chainCodeCommitment = sessionData.chainCodeCommitment;
341+
}
342+
343+
if (sessionData.keyShareBuff) {
344+
dkg.keyShareBuff = sessionData.keyShareBuff;
345+
}
346+
347+
dkg._restoreSession();
348+
return dkg;
349+
}
279350
}

modules/sdk-lib-mpc/test/unit/tss/ecdsa/dklsDkg.ts

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,4 +282,210 @@ describe('DKLS Dkg 2x3', function () {
282282
assert.deepEqual(DklsTypes.getCommonKeychain(userKeyShare), DklsTypes.getCommonKeychain(bitgoKeyShare));
283283
assert.deepEqual(DklsTypes.getCommonKeychain(backupKeyShare), DklsTypes.getCommonKeychain(bitgoKeyShare));
284284
});
285+
286+
it('should successfully finish DKG using restored sessions', async function () {
287+
const user = new DklsDkg.Dkg(3, 2, 0);
288+
const backup = new DklsDkg.Dkg(3, 2, 1);
289+
const bitgo = new DklsDkg.Dkg(3, 2, 2);
290+
291+
// Generate GPG keys for authenticated encryption
292+
openpgp.config.rejectCurves = new Set();
293+
const userKey = await openpgp.generateKey({
294+
userIDs: [{ name: 'user', email: '[email protected]' }],
295+
curve: 'secp256k1',
296+
});
297+
const bitgoKey = await openpgp.generateKey({
298+
userIDs: [{ name: 'bitgo', email: '[email protected]' }],
299+
curve: 'secp256k1',
300+
});
301+
const backupKey = await openpgp.generateKey({
302+
userIDs: [{ name: 'backup', email: '[email protected]' }],
303+
curve: 'secp256k1',
304+
});
305+
306+
const userGpgPubKey: PartyGpgKey = { partyId: 0, gpgKey: userKey.publicKey };
307+
const userGpgPrvKey: PartyGpgKey = { partyId: 0, gpgKey: userKey.privateKey };
308+
const bitgoGpgPubKey: PartyGpgKey = { partyId: 2, gpgKey: bitgoKey.publicKey };
309+
const bitgoGpgPrvKey: PartyGpgKey = { partyId: 2, gpgKey: bitgoKey.privateKey };
310+
const backupGpgPubKey: PartyGpgKey = { partyId: 1, gpgKey: backupKey.publicKey };
311+
const backupGpgPrvKey: PartyGpgKey = { partyId: 1, gpgKey: backupKey.privateKey };
312+
313+
// Initialize DKG and get first round messages
314+
const userRound1Message = await user.initDkg();
315+
const backupRound1Message = await backup.initDkg();
316+
const bitgoRound1Message = await bitgo.initDkg();
317+
318+
// Process round 1 messages to advance to round 2
319+
let serializedMessages = serializeMessages({
320+
broadcastMessages: [userRound1Message, backupRound1Message],
321+
p2pMessages: [],
322+
});
323+
324+
let authEncMessages = await encryptAndAuthOutgoingMessages(
325+
serializedMessages,
326+
[bitgoGpgPubKey],
327+
[userGpgPrvKey, backupGpgPrvKey]
328+
);
329+
330+
const restoredRound1User = await DklsDkg.Dkg.restoreSession(3, 2, 0, user.getSessionData());
331+
const restoredRound1Backup = await DklsDkg.Dkg.restoreSession(3, 2, 1, backup.getSessionData());
332+
const restoredRound1Bitgo = await DklsDkg.Dkg.restoreSession(3, 2, 2, bitgo.getSessionData());
333+
334+
// Round 2
335+
const userRound2Messages = restoredRound1User.handleIncomingMessages({
336+
p2pMessages: [],
337+
broadcastMessages: [bitgoRound1Message, backupRound1Message],
338+
});
339+
const userRound2Data = restoredRound1User.getSessionData();
340+
341+
const backupRound2Messages = restoredRound1Backup.handleIncomingMessages({
342+
p2pMessages: [],
343+
broadcastMessages: [userRound1Message, bitgoRound1Message],
344+
});
345+
const backupRound2Data = restoredRound1Backup.getSessionData();
346+
347+
const decryptedMessages = await decryptAndVerifyIncomingMessages(
348+
authEncMessages,
349+
[userGpgPubKey, backupGpgPubKey],
350+
[bitgoGpgPrvKey]
351+
);
352+
353+
const deserializedDecryptedMessages = deserializeMessages(decryptedMessages);
354+
const bitgoRound2Messages = restoredRound1Bitgo.handleIncomingMessages(deserializedDecryptedMessages);
355+
const bitgoRound2Data = restoredRound1Bitgo.getSessionData();
356+
357+
// Restore sessions for Round 3
358+
const restoredRound2User = await DklsDkg.Dkg.restoreSession(3, 2, 0, userRound2Data);
359+
const restoredRound2Backup = await DklsDkg.Dkg.restoreSession(3, 2, 1, backupRound2Data);
360+
const restoredRound2Bitgo = await DklsDkg.Dkg.restoreSession(3, 2, 2, bitgoRound2Data);
361+
362+
// Round 3
363+
const restoredUserRound3Messages = restoredRound2User.handleIncomingMessages({
364+
p2pMessages: backupRound2Messages.p2pMessages
365+
.filter((m) => m.to === 0)
366+
.concat(bitgoRound2Messages.p2pMessages.filter((m) => m.to === 0)),
367+
broadcastMessages: [],
368+
});
369+
const userRound3Data = restoredRound2User.getSessionData();
370+
371+
const restoredBackupRound3Messages = restoredRound2Backup.handleIncomingMessages({
372+
p2pMessages: bitgoRound2Messages.p2pMessages
373+
.filter((m) => m.to === 1)
374+
.concat(userRound2Messages.p2pMessages.filter((m) => m.to === 1)),
375+
broadcastMessages: [],
376+
});
377+
const backupRound3Data = restoredRound2Backup.getSessionData();
378+
379+
// Encrypt messages for bitgo
380+
serializedMessages = serializeMessages({
381+
broadcastMessages: [],
382+
p2pMessages: userRound2Messages.p2pMessages
383+
.filter((m) => m.to === 2)
384+
.concat(backupRound2Messages.p2pMessages.filter((m) => m.to === 2)),
385+
});
386+
387+
authEncMessages = await encryptAndAuthOutgoingMessages(
388+
serializedMessages,
389+
[bitgoGpgPubKey],
390+
[userGpgPrvKey, backupGpgPrvKey]
391+
);
392+
393+
const restoredBitgoRound3Messages = restoredRound2Bitgo.handleIncomingMessages(
394+
deserializeMessages(
395+
await decryptAndVerifyIncomingMessages(authEncMessages, [userGpgPubKey, backupGpgPubKey], [bitgoGpgPrvKey])
396+
)
397+
);
398+
const bitgoRound3Data = restoredRound2Bitgo.getSessionData();
399+
400+
// Restore sessions for Round 4
401+
const restoredRound3User = await DklsDkg.Dkg.restoreSession(3, 2, 0, userRound3Data);
402+
const restoredRound3Backup = await DklsDkg.Dkg.restoreSession(3, 2, 1, backupRound3Data);
403+
const restoredRound3Bitgo = await DklsDkg.Dkg.restoreSession(3, 2, 2, bitgoRound3Data);
404+
405+
// Round 4
406+
const restoredUserRound4Messages = restoredRound3User.handleIncomingMessages({
407+
p2pMessages: restoredBackupRound3Messages.p2pMessages
408+
.filter((m) => m.to === 0)
409+
.concat(restoredBitgoRound3Messages.p2pMessages.filter((m) => m.to === 0)),
410+
broadcastMessages: [],
411+
});
412+
const userRound4Data = restoredRound3User.getSessionData();
413+
414+
const restoredBackupRound4Messages = restoredRound3Backup.handleIncomingMessages({
415+
p2pMessages: restoredBitgoRound3Messages.p2pMessages
416+
.filter((m) => m.to === 1)
417+
.concat(restoredUserRound3Messages.p2pMessages.filter((m) => m.to === 1)),
418+
broadcastMessages: [],
419+
});
420+
const backupRound4Data = restoredRound3Backup.getSessionData();
421+
422+
serializedMessages = serializeMessages({
423+
broadcastMessages: [],
424+
p2pMessages: restoredUserRound3Messages.p2pMessages
425+
.filter((m) => m.to === 2)
426+
.concat(restoredBackupRound3Messages.p2pMessages.filter((m) => m.to === 2)),
427+
});
428+
429+
authEncMessages = await encryptAndAuthOutgoingMessages(
430+
serializedMessages,
431+
[bitgoGpgPubKey],
432+
[userGpgPrvKey, backupGpgPrvKey]
433+
);
434+
435+
const restoredBitgoRound4Messages = restoredRound3Bitgo.handleIncomingMessages(
436+
deserializeMessages(
437+
await decryptAndVerifyIncomingMessages(authEncMessages, [userGpgPubKey, backupGpgPubKey], [bitgoGpgPrvKey])
438+
)
439+
);
440+
const bitgoRound4Data = restoredRound3Bitgo.getSessionData();
441+
442+
// Restore sessions for final messages
443+
const restoredRound4User = await DklsDkg.Dkg.restoreSession(3, 2, 0, userRound4Data);
444+
const restoredRound4Backup = await DklsDkg.Dkg.restoreSession(3, 2, 1, backupRound4Data);
445+
const restoredRound4Bitgo = await DklsDkg.Dkg.restoreSession(3, 2, 2, bitgoRound4Data);
446+
447+
// Final messages
448+
restoredRound4User.handleIncomingMessages({
449+
p2pMessages: [],
450+
broadcastMessages: restoredBitgoRound4Messages.broadcastMessages.concat(
451+
restoredBackupRound4Messages.broadcastMessages
452+
),
453+
});
454+
455+
serializedMessages = serializeMessages({
456+
broadcastMessages: restoredBackupRound4Messages.broadcastMessages.concat(
457+
restoredUserRound4Messages.broadcastMessages
458+
),
459+
p2pMessages: [],
460+
});
461+
462+
authEncMessages = await encryptAndAuthOutgoingMessages(
463+
serializedMessages,
464+
[bitgoGpgPubKey],
465+
[userGpgPrvKey, backupGpgPrvKey]
466+
);
467+
468+
restoredRound4Bitgo.handleIncomingMessages(
469+
deserializeMessages(
470+
await decryptAndVerifyIncomingMessages(authEncMessages, [userGpgPubKey, backupGpgPubKey], [bitgoGpgPrvKey])
471+
)
472+
);
473+
474+
restoredRound4Backup.handleIncomingMessages({
475+
p2pMessages: [],
476+
broadcastMessages: restoredBitgoRound4Messages.broadcastMessages.concat(
477+
restoredUserRound4Messages.broadcastMessages
478+
),
479+
});
480+
481+
// Verify key shares
482+
const userKeyShare = restoredRound4User.getKeyShare();
483+
const backupKeyShare = restoredRound4Backup.getKeyShare();
484+
const bitgoKeyShare = restoredRound4Bitgo.getKeyShare();
485+
486+
assert.deepEqual(decode(userKeyShare).public_key, decode(bitgoKeyShare).public_key);
487+
assert.deepEqual(decode(backupKeyShare).public_key, decode(bitgoKeyShare).public_key);
488+
assert.deepEqual(DklsTypes.getCommonKeychain(userKeyShare), DklsTypes.getCommonKeychain(bitgoKeyShare));
489+
assert.deepEqual(DklsTypes.getCommonKeychain(backupKeyShare), DklsTypes.getCommonKeychain(bitgoKeyShare));
490+
});
285491
});

0 commit comments

Comments
 (0)