From b2ca8dfb7f88d297ddc07a73e30844da491a5f94 Mon Sep 17 00:00:00 2001 From: Marzooqa Naeema Kather Date: Mon, 27 Apr 2026 19:07:41 +0530 Subject: [PATCH] fix: lazy-load wasm-mps to fix browser wasn initialisation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Static top-level import compiled to synchronous require() in CJS, which triggered fs.readFileSync on the .wasm binary at module load time. fs does not exist in browser environments, so all WASM functions were undefined when called from sdk-core. Mirror the ECDSA DKG pattern (ecdsa-dkls/dkg.ts): - Remove static top-level import of @bitgo/wasm-mps - Add wasmMps field with optional constructor injection for DI - Add private loadWasmMps() that lazily does await import('@bitgo/wasm-mps') (bundler resolves to ESM in browser, CJS in Node — no typeof window check needed as wasm-mps ships a single package with dual exports) - Make initDkg() async; WASM is loaded once on first call - All WASM calls go through getWasmMps() which throws if not loaded - Add await to initDkg() calls in sdk-core/eddsaMPCv2.ts - Update tests to await initDkg() and mark affected callbacks async Co-Authored-By: Claude Sonnet 4.6 TICKET: WCI-244 --- .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 4 +- modules/sdk-lib-mpc/.mocharc.js | 1 + modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts | 39 ++++++++++++++----- .../sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts | 36 ++++++++--------- .../sdk-lib-mpc/test/unit/tss/eddsa/util.ts | 6 +-- 5 files changed, 54 insertions(+), 32 deletions(-) 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 = [