-
Notifications
You must be signed in to change notification settings - Fork 274
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(sdk-lib-mpc): support DKLS DKG primitives
Ticket: HSM-267
- Loading branch information
1 parent
d219a48
commit 0f3808e
Showing
6 changed files
with
365 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { KeygenSession, Keyshare, Message } from '@silencelaboratories/dkls-wasm-ll-node'; | ||
import { DeserializedBroadcastMessage, DeserializedMessages, DkgState } from './types'; | ||
import { decode } from 'cbor-x'; | ||
|
||
export class Dkg { | ||
protected dkgSession: KeygenSession; | ||
protected dkgKeyShare: Keyshare; | ||
protected n: number; | ||
protected t: number; | ||
protected chainCodeCommitment: Uint8Array | undefined; | ||
protected partyIdx: number; | ||
protected dkgState: DkgState = DkgState.Uninitialized; | ||
|
||
constructor(n: number, t: number, partyIdx: number) { | ||
this.n = n; | ||
this.t = t; | ||
this.partyIdx = partyIdx; | ||
this.chainCodeCommitment = undefined; | ||
} | ||
|
||
private _deserializeState() { | ||
const round = decode(this.dkgSession.toBytes()).round; | ||
switch (round) { | ||
case 'WaitMsg1': | ||
this.dkgState = DkgState.Round1; | ||
break; | ||
case 'WaitMsg2': | ||
this.dkgState = DkgState.Round2; | ||
break; | ||
case 'WaitMsg3': | ||
this.dkgState = DkgState.Round3; | ||
break; | ||
case 'WaitMsg4': | ||
this.dkgState = DkgState.Round4; | ||
break; | ||
case 'Ended': | ||
this.dkgState = DkgState.Complete; | ||
break; | ||
default: | ||
this.dkgState = DkgState.InvalidState; | ||
throw `Invalid State: ${round}`; | ||
} | ||
} | ||
|
||
async initDkg(): Promise<DeserializedBroadcastMessage> { | ||
if (this.t > this.n || this.partyIdx >= this.n) { | ||
throw 'Invalid parameters for DKG'; | ||
} | ||
if (this.dkgState != DkgState.Uninitialized) { | ||
throw 'DKG session already initialized'; | ||
} | ||
this.dkgSession = new KeygenSession(this.n, this.t, this.partyIdx); | ||
try { | ||
const payload = this.dkgSession.createFirstMessage().payload; | ||
this._deserializeState(); | ||
return { | ||
payload: payload, | ||
from: this.partyIdx, | ||
}; | ||
} catch (e) { | ||
throw `Error while creating the first message from party ${this.partyIdx}: ${e}`; | ||
} | ||
} | ||
|
||
getKeyShare(): Buffer { | ||
const keyShareBuff = Buffer.from(this.dkgKeyShare.toBytes()); | ||
this.dkgKeyShare.free(); | ||
return keyShareBuff; | ||
} | ||
|
||
handleIncomingMessages(messagesForIthRound: DeserializedMessages): DeserializedMessages { | ||
try { | ||
let nextRoundMessages: Message[]; | ||
if (this.dkgState == DkgState.Round3) { | ||
const commitmentsUnsorted = messagesForIthRound.p2pMessages | ||
.map((m) => { | ||
return { from: m.from, commitment: m.commitment }; | ||
}) | ||
.concat([{ from: this.partyIdx, commitment: this.chainCodeCommitment }]); | ||
const commitmentsSorted = commitmentsUnsorted | ||
.sort((a, b) => { | ||
return a.from - b.from; | ||
}) | ||
.map((c) => c.commitment); | ||
nextRoundMessages = this.dkgSession.handleMessages( | ||
messagesForIthRound.broadcastMessages | ||
.map((m) => new Message(m.payload, m.from, undefined)) | ||
.concat(messagesForIthRound.p2pMessages.map((m) => new Message(m.payload, m.from, m.to))), | ||
commitmentsSorted | ||
); | ||
} else { | ||
nextRoundMessages = this.dkgSession.handleMessages( | ||
messagesForIthRound.broadcastMessages | ||
.map((m) => new Message(m.payload, m.from, undefined)) | ||
.concat(messagesForIthRound.p2pMessages.map((m) => new Message(m.payload, m.from, m.to))), | ||
undefined | ||
); | ||
} | ||
if (this.dkgState == DkgState.Round4) { | ||
this.dkgKeyShare = this.dkgSession.keyshare(); | ||
this.dkgState = DkgState.Complete; | ||
return { broadcastMessages: [], p2pMessages: [] }; | ||
} else { | ||
// Update ronud data. | ||
this._deserializeState(); | ||
} | ||
if (this.dkgState == DkgState.Round3) { | ||
this.chainCodeCommitment = this.dkgSession.calculateChainCodeCommitment(); | ||
} | ||
const nextRoundSerializedMessages = { | ||
p2pMessages: nextRoundMessages | ||
.filter((m) => m.to_id !== undefined) | ||
.map((m) => { | ||
const p2pReturn = { | ||
payload: m.payload, | ||
from: m.from_id, | ||
to: m.to_id!, | ||
commitment: this.chainCodeCommitment, | ||
}; | ||
return p2pReturn; | ||
}), | ||
broadcastMessages: nextRoundMessages | ||
.filter((m) => m.to_id === undefined) | ||
.map((m) => { | ||
const broadcastReturn = { | ||
payload: m.payload, | ||
from: m.from_id, | ||
}; | ||
return broadcastReturn; | ||
}), | ||
}; | ||
nextRoundMessages.forEach((m) => m.free()); | ||
return nextRoundSerializedMessages; | ||
} catch (e) { | ||
throw `Error while creating messages from party ${this.partyIdx}, round ${this.dkgState - 1}: ${e}`; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * as DklsDkg from './dkg'; | ||
export * as DklsTypes from './types'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
// Broadcast message meant to be sent to multiple parties | ||
interface BroadcastMessage<T> { | ||
payload: T; | ||
from: number; | ||
} | ||
|
||
// P2P message meant to be sent to a specific party | ||
interface P2PMessage<T> { | ||
payload: T; | ||
from: number; | ||
commitment?: T; | ||
to: number; | ||
} | ||
|
||
export enum DkgState { | ||
Uninitialized = 1, | ||
Round1, | ||
Round2, | ||
Round3, | ||
Round4, | ||
Complete, | ||
InvalidState, | ||
} | ||
|
||
export type SerializedBroadcastMessage = BroadcastMessage<string>; | ||
export type DeserializedBroadcastMessage = BroadcastMessage<Uint8Array>; | ||
export type SerializedP2PMessage = P2PMessage<string>; | ||
export type DeserializedP2PMessage = P2PMessage<Uint8Array>; | ||
export type SerializedMessages = { | ||
p2pMessages: SerializedP2PMessage[]; | ||
broadcastMessages: SerializedBroadcastMessage[]; | ||
}; | ||
export type DeserializedMessages = { | ||
p2pMessages: DeserializedP2PMessage[]; | ||
broadcastMessages: DeserializedBroadcastMessage[]; | ||
}; | ||
|
||
/** | ||
* Serializes messages payloads to base64 strings. | ||
* @param messages | ||
*/ | ||
export function serializeMessages(messages: DeserializedMessages): SerializedMessages { | ||
return { | ||
p2pMessages: messages.p2pMessages.map((m) => { | ||
return { | ||
to: m.to, | ||
from: m.from, | ||
payload: Buffer.from(m.payload).toString('base64'), | ||
commitment: m.commitment ? Buffer.from(m.commitment).toString('base64') : m.commitment, | ||
}; | ||
}), | ||
broadcastMessages: messages.broadcastMessages.map((m) => { | ||
return { | ||
from: m.from, | ||
payload: Buffer.from(m.payload).toString('base64'), | ||
}; | ||
}), | ||
}; | ||
} | ||
|
||
/** | ||
* Desrializes messages payloads to Uint8Array. | ||
* @param messages | ||
*/ | ||
export function deserializeMessages(messages: SerializedMessages): DeserializedMessages { | ||
return { | ||
p2pMessages: messages.p2pMessages.map((m) => { | ||
return { | ||
to: m.to, | ||
from: m.from, | ||
payload: new Uint8Array(Buffer.from(m.payload, 'base64')), | ||
commitment: m.commitment ? new Uint8Array(Buffer.from(m.commitment, 'base64')) : undefined, | ||
}; | ||
}), | ||
broadcastMessages: messages.broadcastMessages.map((m) => { | ||
return { | ||
from: m.from, | ||
payload: new Uint8Array(Buffer.from(m.payload, 'base64')), | ||
}; | ||
}), | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { DklsDkg } from '../../../../src/tss/ecdsa-dkls'; | ||
|
||
describe('DKLS Dkg 2x3', function () { | ||
it(`should create key shares`, async function () { | ||
const user = new DklsDkg.Dkg(3, 2, 0); | ||
const backup = new DklsDkg.Dkg(3, 2, 1); | ||
const bitgo = new DklsDkg.Dkg(3, 2, 2); | ||
const userRound1Message = await user.initDkg(); | ||
const backupRound1Message = await backup.initDkg(); | ||
const bitgoRound1Message = await bitgo.initDkg(); | ||
const userRound2Messages = user.handleIncomingMessages({ | ||
p2pMessages: [], | ||
broadcastMessages: [bitgoRound1Message, backupRound1Message], | ||
}); | ||
const backupRound2Messages = backup.handleIncomingMessages({ | ||
p2pMessages: [], | ||
broadcastMessages: [userRound1Message, bitgoRound1Message], | ||
}); | ||
const bitgoRound2Messages = bitgo.handleIncomingMessages({ | ||
p2pMessages: [], | ||
broadcastMessages: [userRound1Message, backupRound1Message], | ||
}); | ||
const userRound3Messages = user.handleIncomingMessages({ | ||
p2pMessages: backupRound2Messages.p2pMessages | ||
.filter((m) => m.to == 0) | ||
.concat(bitgoRound2Messages.p2pMessages.filter((m) => m.to == 0)), | ||
broadcastMessages: [], | ||
}); | ||
const backupRound3Messages = backup.handleIncomingMessages({ | ||
p2pMessages: bitgoRound2Messages.p2pMessages | ||
.filter((m) => m.to == 1) | ||
.concat(userRound2Messages.p2pMessages.filter((m) => m.to == 1)), | ||
broadcastMessages: [], | ||
}); | ||
const bitgoRound3Messages = bitgo.handleIncomingMessages({ | ||
p2pMessages: backupRound2Messages.p2pMessages | ||
.filter((m) => m.to == 2) | ||
.concat(userRound2Messages.p2pMessages.filter((m) => m.to == 2)), | ||
broadcastMessages: [], | ||
}); | ||
const userRound4Messages = user.handleIncomingMessages({ | ||
p2pMessages: backupRound3Messages.p2pMessages | ||
.filter((m) => m.to == 0) | ||
.concat(bitgoRound3Messages.p2pMessages.filter((m) => m.to == 0)), | ||
broadcastMessages: [], | ||
}); | ||
const backupRound4Messages = backup.handleIncomingMessages({ | ||
p2pMessages: bitgoRound3Messages.p2pMessages | ||
.filter((m) => m.to == 1) | ||
.concat(userRound3Messages.p2pMessages.filter((m) => m.to == 1)), | ||
broadcastMessages: [], | ||
}); | ||
const bitgoRound4Messages = bitgo.handleIncomingMessages({ | ||
p2pMessages: backupRound3Messages.p2pMessages | ||
.filter((m) => m.to == 2) | ||
.concat(userRound3Messages.p2pMessages.filter((m) => m.to == 2)), | ||
broadcastMessages: [], | ||
}); | ||
user.handleIncomingMessages({ | ||
p2pMessages: [], | ||
broadcastMessages: bitgoRound4Messages.broadcastMessages.concat(backupRound4Messages.broadcastMessages), | ||
}); | ||
bitgo.handleIncomingMessages({ | ||
p2pMessages: [], | ||
broadcastMessages: backupRound4Messages.broadcastMessages.concat(userRound4Messages.broadcastMessages), | ||
}); | ||
backup.handleIncomingMessages({ | ||
p2pMessages: [], | ||
broadcastMessages: bitgoRound4Messages.broadcastMessages.concat(userRound4Messages.broadcastMessages), | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.