Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
const backupDkg = new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.BACKUP);

// #region round 1
userDkg.initDkg(userSk, [backupPk, bitgoPk]);
backupDkg.initDkg(backupSk, [userPk, bitgoPk]);
await userDkg.initDkg(userSk, [backupPk, bitgoPk]);
await backupDkg.initDkg(backupSk, [userPk, bitgoPk]);

const userMsg1 = userDkg.getFirstMessage();
const backupMsg1 = backupDkg.getFirstMessage();
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-lib-mpc/.mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ module.exports = {
exit: true,
spec: ['test/unit/**/*.ts'],
extension: ['.js', '.ts'],
'node-option': ['experimental-wasm-modules'],
};
39 changes: 30 additions & 9 deletions modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { ed25519_dkg_round0_process, ed25519_dkg_round1_process, ed25519_dkg_round2_process } from '@bitgo/wasm-mps';
import type { MsgState, Share } from '@bitgo/wasm-mps';
import { encode } from 'cbor-x';
import crypto from 'crypto';
import { DeserializedMessage, DeserializedMessages, DkgState, EddsaReducedKeyShare } from './types';

type WasmMps = typeof import('@bitgo/wasm-mps');

/**
* EdDSA Distributed Key Generation (DKG) implementation using @bitgo/wasm-mps.
*
Expand All @@ -14,7 +16,7 @@ import { DeserializedMessage, DeserializedMessages, DkgState, EddsaReducedKeySha
* ```typescript
* const dkg = new DKG(3, 2, 0);
* // X25519 keys come from GPG encryption subkeys (extracted by the orchestrator)
* dkg.initDkg(myX25519PrivKey, [otherParty1X25519PubKey, otherParty2X25519PubKey]);
* await dkg.initDkg(myX25519PrivKey, [otherParty1X25519PubKey, otherParty2X25519PubKey]);
* const msg1 = dkg.getFirstMessage();
* const msg2s = dkg.handleIncomingMessages(allThreeMsg1s);
* dkg.handleIncomingMessages(allThreeMsg2s); // completes DKG
Expand All @@ -38,6 +40,8 @@ export class DKG {
private sharePk: Buffer | null = null;
/** 32-byte chain code from round2 */
private shareChaincode: Buffer | null = null;
/** Lazily loaded WASM module */
private wasmMps: WasmMps | null = null;

protected dkgState: DkgState = DkgState.Uninitialized;

Expand All @@ -47,6 +51,19 @@ export class DKG {
this.partyIdx = partyIdx;
}

private async loadWasmMps(): Promise<void> {
if (!this.wasmMps) {
this.wasmMps = await import('@bitgo/wasm-mps');
}
}

private getWasmMps(): WasmMps {
if (!this.wasmMps) {
throw Error('WASM module not loaded');
}
return this.wasmMps;
}

getState(): DkgState {
return this.dkgState;
}
Expand All @@ -59,7 +76,8 @@ export class DKG {
* @param otherEncPublicKeys - Other parties' 32-byte X25519 public keys, sorted by ascending
* party index (excluding own). For a 3-party setup, this is [party_A_pub, party_B_pub].
*/
initDkg(decryptionKey: Buffer, otherEncPublicKeys: Buffer[]): void {
async initDkg(decryptionKey: Buffer, otherEncPublicKeys: Buffer[]): Promise<void> {
await this.loadWasmMps();
if (!decryptionKey || decryptionKey.length !== 32) {
throw Error('Missing or invalid decryption key: must be 32 bytes');
}
Expand Down Expand Up @@ -87,9 +105,10 @@ export class DKG {
}

const seed = dkgSeed ?? crypto.randomBytes(32);
let result;
const wasm = this.getWasmMps();
let result: MsgState;
try {
result = ed25519_dkg_round0_process(this.partyIdx, this.decryptionKey!, this.otherPubKeys!, seed);
result = wasm.ed25519_dkg_round0_process(this.partyIdx, this.decryptionKey!, this.otherPubKeys!, seed);
} catch (err) {
throw new Error(`Error while creating the first message from party ${this.partyIdx}: ${err}`);
}
Expand Down Expand Up @@ -133,10 +152,12 @@ export class DKG {
.sort((a, b) => a.from - b.from)
.map((m) => m.payload);

const wasm = this.getWasmMps();

if (this.dkgState === DkgState.WaitMsg1) {
let result;
let result: MsgState;
try {
result = ed25519_dkg_round1_process(otherMsgs, this.dkgStateBytes!);
result = wasm.ed25519_dkg_round1_process(otherMsgs, this.dkgStateBytes!);
} catch (err) {
throw new Error(`Error while creating messages from party ${this.partyIdx}, round ${this.dkgState}: ${err}`);
}
Expand All @@ -147,9 +168,9 @@ export class DKG {
}

if (this.dkgState === DkgState.WaitMsg2) {
let share;
let share: Share;
try {
share = ed25519_dkg_round2_process(otherMsgs, this.dkgStateBytes!);
share = wasm.ed25519_dkg_round2_process(otherMsgs, this.dkgStateBytes!);
} catch (err) {
throw new Error(`Error while creating messages from party ${this.partyIdx}, round ${this.dkgState}: ${err}`);
}
Expand Down
36 changes: 18 additions & 18 deletions modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ describe('EdDSA MPS DKG', function () {
});

describe('DKG Initialization', function () {
it('should initialize DKG sessions for all parties', function () {
user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);
it('should initialize DKG sessions for all parties', async function () {
await user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
await backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
await bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);

const userMessage = user.getFirstMessage();
const backupMessage = backup.getFirstMessage();
Expand Down Expand Up @@ -63,13 +63,13 @@ describe('EdDSA MPS DKG', function () {
});

describe('DKG Protocol Execution', function () {
beforeEach(function () {
user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);
beforeEach(async function () {
await user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
await backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
await bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);
});

it('should complete full DKG protocol and generate key shares', function () {
it('should complete full DKG protocol and generate key shares', async function () {
const r1Messages = [user.getFirstMessage(), backup.getFirstMessage(), bitgo.getFirstMessage()];

assert.strictEqual(r1Messages.length, 3, 'Should have 3 round 1 messages');
Expand Down Expand Up @@ -109,7 +109,7 @@ describe('EdDSA MPS DKG', function () {
assert(Buffer.isBuffer(bitgoKeyShare) && bitgoKeyShare.length > 0, 'BitGo key share should be non-empty Buffer');
});

it('should generate consistent public keys across all parties', function () {
it('should generate consistent public keys across all parties', async function () {
const r1Messages = [user.getFirstMessage(), backup.getFirstMessage(), bitgo.getFirstMessage()];
const r2Messages = [
...user.handleIncomingMessages(r1Messages),
Expand Down Expand Up @@ -205,13 +205,13 @@ describe('EdDSA MPS DKG', function () {
});

describe('Message Serialization', function () {
it('should serialize and deserialize messages round-trip', function () {
it('should serialize and deserialize messages round-trip', async function () {
userKP = makeKeypair();
backupKP = makeKeypair();
bitgoKP = makeKeypair();
user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);
await user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
await backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
await bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);

const r1Messages = [user.getFirstMessage(), backup.getFirstMessage(), bitgo.getFirstMessage()];

Expand All @@ -231,10 +231,10 @@ describe('EdDSA MPS DKG', function () {
});

describe('Session Management', function () {
it('should export and restore DKG session and continue protocol correctly', function () {
user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);
it('should export and restore DKG session and continue protocol correctly', async function () {
await user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
await backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
await bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);

user.getFirstMessage();
backup.getFirstMessage();
Expand Down
6 changes: 3 additions & 3 deletions modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ export async function generateEdDsaDKGKeyShares(
const bitgoKP = generateX25519Keypair(seedBitgo);

// Each party gets own privKey + other parties' pubKeys sorted by ascending party index
user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);
await user.initDkg(userKP.privKey, [backupKP.pubKey, bitgoKP.pubKey]);
await backup.initDkg(backupKP.privKey, [userKP.pubKey, bitgoKP.pubKey]);
await bitgo.initDkg(bitgoKP.privKey, [userKP.pubKey, backupKP.pubKey]);

// Use seed as DKG round0 seed for determinism when seed is provided
const r1Messages = [
Expand Down
Loading