diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index 2e98b8975d..f93984e8cf 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -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(); diff --git a/modules/sdk-lib-mpc/.mocharc.js b/modules/sdk-lib-mpc/.mocharc.js index 8c89365d62..2e3daae7f8 100644 --- a/modules/sdk-lib-mpc/.mocharc.js +++ b/modules/sdk-lib-mpc/.mocharc.js @@ -8,4 +8,5 @@ module.exports = { exit: true, spec: ['test/unit/**/*.ts'], extension: ['.js', '.ts'], + 'node-option': ['experimental-wasm-modules'], }; diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts index 4f9b95eaf2..6845f78a6e 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts @@ -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. * @@ -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 @@ -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; @@ -47,6 +51,19 @@ export class DKG { this.partyIdx = partyIdx; } + private async loadWasmMps(): Promise { + 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; } @@ -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 { + await this.loadWasmMps(); if (!decryptionKey || decryptionKey.length !== 32) { throw Error('Missing or invalid decryption key: must be 32 bytes'); } @@ -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}`); } @@ -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}`); } @@ -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}`); } diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts index 4d2787b6e0..8c9b98e6eb 100644 --- a/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts @@ -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(); @@ -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'); @@ -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), @@ -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()]; @@ -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(); diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts index 237f2b12a3..471f03e8fe 100644 --- a/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/util.ts @@ -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 = [