Skip to content

Commit d5bc087

Browse files
Support Botbitgobot
authored andcommitted
feat(dkg): 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 in walletGenerationCallbacks.ts (WCI-895): the factory 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's createKeychainsWithExternalSigner orchestrator (WCI-916). This replaces the WASM/GPG/DKG logic that currently runs inline in EddsaMPCv2Utils.createKeychains with AWM-delegated calls, enabling hardware-backed key generation in the AKM pattern. Local EddsaMPCv2KeyGenCallbacks type definitions mirror the shape from BitGoJS WCI-894 and 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 f82bca9 commit d5bc087

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)