Skip to content

Commit 5f093c1

Browse files
author
Support Bot
committed
feat(mbe): add EdDSA MPCv2 AWM client methods and ME callback factory
Add three methods to AdvancedWalletManagerClient (WCI-893): - eddsaMPCv2KeyGenInitialize - eddsaMPCv2KeyGenRound1 - eddsaMPCv2KeyGenFinalize Each method calls the corresponding AWM endpoint added in the previous PR, with mTLS agent support. Add createEddsaMPCv2KeyGenCallbacks factory in walletGenerationCallbacks.ts (WCI-895): wraps user and backup AWM clients into an EddsaMPCv2KeyGenCallbacks object whose initialize, round1, and finalize callbacks fan both clients out in parallel and merge results for the SDK orchestrator (WCI-916). This replaces the inline WASM/GPG/DKG key gen logic with AWM-delegated calls, enabling hardware-backed key generation in the AKM pattern. Local EddsaMPCv2KeyGenCallbacks type mirrors the WCI-894 shape; it will be replaced by the SDK import once published. Three unit tests verify parallel fan-out and result merging for each callback phase using nock. Ticket: WCI-895 Session-Id: 4df43c40-1cea-4bcc-ae92-080304169427 Task-Id: 04666b37-650f-4c07-97b0-76a62e98f1ad
1 parent 0b9c357 commit 5f093c1

3 files changed

Lines changed: 419 additions & 1 deletion

File tree

src/__tests__/api/master/walletGenerationCallbacks.test.ts

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import {
66
createAwmClient,
77
AdvancedWalletManagerClient,
88
} from '../../../masterBitgoExpress/clients/advancedWalletManagerClient';
9-
import { createOnchainKeyGenCallback } from '../../../masterBitgoExpress/handlers/walletGenerationCallbacks';
9+
import {
10+
createOnchainKeyGenCallback,
11+
createEddsaMPCv2KeyGenCallbacks,
12+
} from '../../../masterBitgoExpress/handlers/walletGenerationCallbacks';
1013
import { AppMode, KeySource, MasterExpressConfig, TlsMode } from '../../../shared/types';
1114
import { DEFAULT_ASYNC_MODE_CONFIG } from './testUtils';
1215

@@ -150,4 +153,148 @@ describe('walletGenerationCallbacks', () => {
150153
}).should.be.rejectedWith('Unexpected key source for onchain key generation: bitgo');
151154
});
152155
});
156+
157+
describe('createEddsaMPCv2KeyGenCallbacks', () => {
158+
const enterprise = 'test-enterprise';
159+
const bitgoPublicGpgKey = 'test-bitgo-gpg-key';
160+
161+
beforeEach(() => {
162+
const config = makeConfig({ advancedWalletManagerBackupUrl: backupAwmUrl });
163+
awmUserClient = createAwmClient(config, coin)!;
164+
awmBackupClient = createAwmBackupClient(config, coin)!;
165+
assert(awmUserClient);
166+
assert(awmBackupClient);
167+
});
168+
169+
it('initializeCallback fans out to user and backup AWM in parallel', async () => {
170+
const userInitNock = nock(advancedWalletManagerUrl)
171+
.post(`/api/${coin}/eddsampcv2/keygen/initialize`, {
172+
source: 'user',
173+
enterprise,
174+
bitgoPublicGpgKey,
175+
})
176+
.reply(200, {
177+
gpgPublicKey: 'user-gpg-pub',
178+
signedMsg1: { message: 'user-msg1', signature: 'user-sig1' },
179+
encryptedState: 'user-enc-state',
180+
encryptedStateKey: 'user-enc-state-key',
181+
});
182+
183+
const backupInitNock = nock(backupAwmUrl)
184+
.post(`/api/${coin}/eddsampcv2/keygen/initialize`, {
185+
source: 'backup',
186+
enterprise,
187+
bitgoPublicGpgKey,
188+
})
189+
.reply(200, {
190+
gpgPublicKey: 'backup-gpg-pub',
191+
signedMsg1: { message: 'backup-msg1', signature: 'backup-sig1' },
192+
encryptedState: 'backup-enc-state',
193+
encryptedStateKey: 'backup-enc-state-key',
194+
});
195+
196+
const callbacks = createEddsaMPCv2KeyGenCallbacks(awmUserClient, awmBackupClient);
197+
const result = await callbacks.initializeCallback({ enterprise, bitgoPublicGpgKey });
198+
199+
result.userGpgPublicKey.should.equal('user-gpg-pub');
200+
result.backupGpgPublicKey.should.equal('backup-gpg-pub');
201+
result.userSignedMsg1.message.should.equal('user-msg1');
202+
result.backupSignedMsg1.message.should.equal('backup-msg1');
203+
result.userEncryptedState.should.equal('user-enc-state');
204+
result.backupEncryptedState.should.equal('backup-enc-state');
205+
userInitNock.done();
206+
backupInitNock.done();
207+
});
208+
209+
it('round1Callback fans out to user and backup AWM in parallel', async () => {
210+
const bitgoMsg1 = { message: 'bitgo-msg1', signature: 'bitgo-sig1' };
211+
212+
const userR1Nock = nock(advancedWalletManagerUrl)
213+
.post(`/api/${coin}/eddsampcv2/keygen/round1`, {
214+
source: 'user',
215+
bitgoMsg1,
216+
encryptedState: 'user-enc-state',
217+
encryptedStateKey: 'user-enc-state-key',
218+
})
219+
.reply(200, {
220+
signedMsg2: { message: 'user-msg2', signature: 'user-sig2' },
221+
encryptedState: 'user-enc-state-r1',
222+
encryptedStateKey: 'user-enc-state-key-r1',
223+
});
224+
225+
const backupR1Nock = nock(backupAwmUrl)
226+
.post(`/api/${coin}/eddsampcv2/keygen/round1`, {
227+
source: 'backup',
228+
bitgoMsg1,
229+
encryptedState: 'backup-enc-state',
230+
encryptedStateKey: 'backup-enc-state-key',
231+
})
232+
.reply(200, {
233+
signedMsg2: { message: 'backup-msg2', signature: 'backup-sig2' },
234+
encryptedState: 'backup-enc-state-r1',
235+
encryptedStateKey: 'backup-enc-state-key-r1',
236+
});
237+
238+
const callbacks = createEddsaMPCv2KeyGenCallbacks(awmUserClient, awmBackupClient);
239+
const result = await callbacks.round1Callback({
240+
bitgoMsg1,
241+
userEncryptedState: 'user-enc-state',
242+
userEncryptedStateKey: 'user-enc-state-key',
243+
backupEncryptedState: 'backup-enc-state',
244+
backupEncryptedStateKey: 'backup-enc-state-key',
245+
});
246+
247+
result.userSignedMsg2.message.should.equal('user-msg2');
248+
result.backupSignedMsg2.message.should.equal('backup-msg2');
249+
result.userEncryptedState.should.equal('user-enc-state-r1');
250+
result.backupEncryptedState.should.equal('backup-enc-state-r1');
251+
userR1Nock.done();
252+
backupR1Nock.done();
253+
});
254+
255+
it('finalizeCallback fans out to user and backup AWM in parallel', async () => {
256+
const bitgoMsg2 = { message: 'bitgo-msg2', signature: 'bitgo-sig2' };
257+
const commonPublicKeychain = 'common-keychain-abc123';
258+
259+
const userFinalizeNock = nock(advancedWalletManagerUrl)
260+
.post(`/api/${coin}/eddsampcv2/keygen/finalize`, {
261+
source: 'user',
262+
bitgoMsg2,
263+
commonPublicKeychain,
264+
encryptedState: 'user-enc-state',
265+
encryptedStateKey: 'user-enc-state-key',
266+
})
267+
.reply(200, {
268+
source: 'user',
269+
commonKeychain: commonPublicKeychain,
270+
});
271+
272+
const backupFinalizeNock = nock(backupAwmUrl)
273+
.post(`/api/${coin}/eddsampcv2/keygen/finalize`, {
274+
source: 'backup',
275+
bitgoMsg2,
276+
commonPublicKeychain,
277+
encryptedState: 'backup-enc-state',
278+
encryptedStateKey: 'backup-enc-state-key',
279+
})
280+
.reply(200, {
281+
source: 'backup',
282+
commonKeychain: commonPublicKeychain,
283+
});
284+
285+
const callbacks = createEddsaMPCv2KeyGenCallbacks(awmUserClient, awmBackupClient);
286+
const result = await callbacks.finalizeCallback({
287+
bitgoMsg2,
288+
commonPublicKeychain,
289+
userEncryptedState: 'user-enc-state',
290+
userEncryptedStateKey: 'user-enc-state-key',
291+
backupEncryptedState: 'backup-enc-state',
292+
backupEncryptedStateKey: 'backup-enc-state-key',
293+
});
294+
295+
result.commonKeychain.should.equal(commonPublicKeychain);
296+
userFinalizeNock.done();
297+
backupFinalizeNock.done();
298+
});
299+
});
153300
});

src/masterBitgoExpress/clients/advancedWalletManagerClient.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ import {
3939
MpcV2InitializeResponseType,
4040
MpcV2RecoveryResponseType,
4141
MpcV2RoundResponseType,
42+
EddsaMPCv2KeyGenInitializeResponseType,
43+
EddsaMPCv2KeyGenRound1ResponseType,
44+
EddsaMPCv2KeyGenFinalizeResponseType,
4245
} from '../../advancedWalletManager/routers/advancedWalletManagerApiSpec';
4346
import { FormattedOfflineVaultTxInfo } from '@bitgo-beta/abstract-utxo';
4447
import { RecoveryTxRequest } from '@bitgo-beta/sdk-core';
@@ -822,6 +825,108 @@ export class AdvancedWalletManagerClient {
822825
}
823826
}
824827

828+
/**
829+
* Initialize EdDSA MPCv2 key generation
830+
*/
831+
async eddsaMPCv2KeyGenInitialize(params: {
832+
source: 'user' | 'backup';
833+
enterprise: string;
834+
bitgoPublicGpgKey: string;
835+
}): Promise<EddsaMPCv2KeyGenInitializeResponseType> {
836+
if (!this.coin) {
837+
throw new Error('Coin must be specified for EdDSA MPCv2 key generation initialize');
838+
}
839+
840+
try {
841+
let request = this.apiClient['v1.eddsampcv2.keygen.initialize'].post({
842+
coin: this.coin,
843+
...params,
844+
});
845+
846+
if (this.tlsMode === TlsMode.MTLS) {
847+
request = request.agent(this.createHttpsAgent());
848+
}
849+
850+
const response = await request.decodeExpecting(200);
851+
return response.body;
852+
} catch (error) {
853+
logger.error(
854+
'Failed to initialize EdDSA MPCv2 key generation: %s',
855+
(error as DecodeError).decodedResponse?.body,
856+
);
857+
throw error;
858+
}
859+
}
860+
861+
/**
862+
* EdDSA MPCv2 key generation round 1
863+
*/
864+
async eddsaMPCv2KeyGenRound1(params: {
865+
source: 'user' | 'backup';
866+
bitgoMsg1: { message: string; signature: string };
867+
encryptedState: string;
868+
encryptedStateKey: string;
869+
}): Promise<EddsaMPCv2KeyGenRound1ResponseType> {
870+
if (!this.coin) {
871+
throw new Error('Coin must be specified for EdDSA MPCv2 key generation round 1');
872+
}
873+
874+
try {
875+
let request = this.apiClient['v1.eddsampcv2.keygen.round1'].post({
876+
coin: this.coin,
877+
...params,
878+
});
879+
880+
if (this.tlsMode === TlsMode.MTLS) {
881+
request = request.agent(this.createHttpsAgent());
882+
}
883+
884+
const response = await request.decodeExpecting(200);
885+
return response.body;
886+
} catch (error) {
887+
logger.error(
888+
'Failed EdDSA MPCv2 key generation round 1: %s',
889+
(error as DecodeError).decodedResponse?.body,
890+
);
891+
throw error;
892+
}
893+
}
894+
895+
/**
896+
* Finalize EdDSA MPCv2 key generation
897+
*/
898+
async eddsaMPCv2KeyGenFinalize(params: {
899+
source: 'user' | 'backup';
900+
bitgoMsg2: { message: string; signature: string };
901+
commonPublicKeychain: string;
902+
encryptedState: string;
903+
encryptedStateKey: string;
904+
}): Promise<EddsaMPCv2KeyGenFinalizeResponseType> {
905+
if (!this.coin) {
906+
throw new Error('Coin must be specified for EdDSA MPCv2 key generation finalize');
907+
}
908+
909+
try {
910+
let request = this.apiClient['v1.eddsampcv2.keygen.finalize'].post({
911+
coin: this.coin,
912+
...params,
913+
});
914+
915+
if (this.tlsMode === TlsMode.MTLS) {
916+
request = request.agent(this.createHttpsAgent());
917+
}
918+
919+
const response = await request.decodeExpecting(200);
920+
return response.body;
921+
} catch (error) {
922+
logger.error(
923+
'Failed to finalize EdDSA MPCv2 key generation: %s',
924+
(error as DecodeError).decodedResponse?.body,
925+
);
926+
throw error;
927+
}
928+
}
929+
825930
async recoverEcdsaMpcV2Wallet(params: {
826931
txHex: string;
827932
pub: string;

0 commit comments

Comments
 (0)