Skip to content

Commit f14db2d

Browse files
Marzooqaclaude
andcommitted
feat(sdk-core): implement EdDSA MPCv2 external signer orchestrator
Implement signEddsaMPCv2TssUsingExternalSigner in EddsaMPCv2Utils, replacing the stub in baseTSSUtils. Also wire up the wallet.ts dispatch block and private signTransactionTssExternalSignerEdDSAMPCv2 method so the calling code and implementation land together. - EddsaMPCv2Utils.signEddsaMPCv2TssUsingExternalSigner: full 3-round orchestration using caller-supplied generator functions, pickBitgo- PubGpgKeyForSigning with isEddsaMpcv2=true, and armored BitGo GPG key passed to round 2/3 generators. Asserts signatureShares after round 2, mirrors ECDSA counterpart. - wallet.ts: adds dispatch block for customEddsaMPCv2Signing* params before existing ECDSA MPCv2 block; adds private signTransactionTssExternalSignerEdDSAMPCv2 method. - Unit tests: 5 cases covering happy path, string txRequestId resolution, missing round-2 signatureShares assert, message-signing rejection, and armored key delivery to generators. Ticket: WCI-374 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Session-Id: 68742945-d54f-4fb6-a866-47a802ad253e Task-Id: bb6a6972-cbe9-43f2-a9c3-74eefc045a44
1 parent abb73a8 commit f14db2d

3 files changed

Lines changed: 463 additions & 2 deletions

File tree

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,18 @@ import {
2626
} from '../../../tss/eddsa/eddsaMPCv2';
2727
import { generateGPGKeyPair } from '../../opengpgUtils';
2828
import { MPCv2PartiesEnum } from '../ecdsa/typesMPCv2';
29-
import { RequestType, SignatureShareType, TSSParamsForMessageWithPrv, TSSParamsWithPrv, TxRequest } from '../baseTypes';
29+
import {
30+
CustomEddsaMPCv2SigningRound1GeneratingFunction,
31+
CustomEddsaMPCv2SigningRound2GeneratingFunction,
32+
CustomEddsaMPCv2SigningRound3GeneratingFunction,
33+
RequestType,
34+
SignatureShareType,
35+
TSSParams,
36+
TSSParamsForMessage,
37+
TSSParamsForMessageWithPrv,
38+
TSSParamsWithPrv,
39+
TxRequest,
40+
} from '../baseTypes';
3041
import { BaseEddsaUtils } from './base';
3142
import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './eddsaMPCv2KeyGenSender';
3243

@@ -515,4 +526,104 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
515526
}
516527

517528
// #endregion
529+
530+
// #region external signer
531+
/** @inheritdoc */
532+
async signEddsaMPCv2TssUsingExternalSigner(
533+
params: TSSParams | TSSParamsForMessage,
534+
externalSignerEddsaMPCv2SigningRound1Generator: CustomEddsaMPCv2SigningRound1GeneratingFunction,
535+
externalSignerEddsaMPCv2SigningRound2Generator: CustomEddsaMPCv2SigningRound2GeneratingFunction,
536+
externalSignerEddsaMPCv2SigningRound3Generator: CustomEddsaMPCv2SigningRound3GeneratingFunction,
537+
requestType: RequestType = RequestType.tx
538+
): Promise<TxRequest> {
539+
const { txRequest, reqId } = params;
540+
541+
// TODO(WP-2176): Add support for message signing
542+
assert(
543+
requestType === RequestType.tx,
544+
'Only transaction signing is supported for external signer, got: ' + requestType
545+
);
546+
547+
let txRequestResolved: TxRequest;
548+
if (typeof txRequest === 'string') {
549+
txRequestResolved = await getTxRequest(this.bitgo, this.wallet.id(), txRequest, reqId);
550+
} else {
551+
txRequestResolved = txRequest;
552+
}
553+
554+
const bitgoPublicGpgKey = await this.pickBitgoPubGpgKeyForSigning(
555+
true,
556+
reqId,
557+
txRequestResolved.enterpriseId,
558+
true
559+
);
560+
561+
if (!bitgoPublicGpgKey) {
562+
throw new Error('Missing BitGo GPG key for MPCv2');
563+
}
564+
565+
// round 1
566+
const { signatureShareRound1, userGpgPubKey, encryptedRound1Session, encryptedUserGpgPrvKey } =
567+
await externalSignerEddsaMPCv2SigningRound1Generator({ txRequest: txRequestResolved });
568+
const round1TxRequest = await sendSignatureShareV2(
569+
this.bitgo,
570+
txRequestResolved.walletId,
571+
txRequestResolved.txRequestId,
572+
[signatureShareRound1],
573+
requestType,
574+
this.baseCoin.getMPCAlgorithm(),
575+
userGpgPubKey,
576+
undefined,
577+
this.wallet.multisigTypeVersion(),
578+
reqId
579+
);
580+
581+
// round 2
582+
const { signatureShareRound2, encryptedRound2Session } =
583+
await externalSignerEddsaMPCv2SigningRound2Generator({
584+
txRequest: round1TxRequest,
585+
encryptedRound1Session,
586+
encryptedUserGpgPrvKey,
587+
bitgoPublicGpgKey: bitgoPublicGpgKey.armor(),
588+
});
589+
const round2TxRequest = await sendSignatureShareV2(
590+
this.bitgo,
591+
txRequestResolved.walletId,
592+
txRequestResolved.txRequestId,
593+
[signatureShareRound2],
594+
requestType,
595+
this.baseCoin.getMPCAlgorithm(),
596+
userGpgPubKey,
597+
undefined,
598+
this.wallet.multisigTypeVersion(),
599+
reqId
600+
);
601+
assert(
602+
round2TxRequest.transactions && round2TxRequest.transactions[0].signatureShares,
603+
'Missing signature shares in round 2 txRequest'
604+
);
605+
606+
// round 3
607+
const { signatureShareRound3 } = await externalSignerEddsaMPCv2SigningRound3Generator({
608+
txRequest: round2TxRequest,
609+
encryptedRound2Session,
610+
encryptedUserGpgPrvKey,
611+
bitgoPublicGpgKey: bitgoPublicGpgKey.armor(),
612+
});
613+
await sendSignatureShareV2(
614+
this.bitgo,
615+
txRequestResolved.walletId,
616+
txRequestResolved.txRequestId,
617+
[signatureShareRound3],
618+
requestType,
619+
this.baseCoin.getMPCAlgorithm(),
620+
userGpgPubKey,
621+
undefined,
622+
this.wallet.multisigTypeVersion(),
623+
reqId
624+
);
625+
626+
return sendTxRequest(this.bitgo, txRequestResolved.walletId, txRequestResolved.txRequestId, requestType, reqId);
627+
}
628+
// #endregion
518629
}

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2165,6 +2165,15 @@ export class Wallet implements IWallet {
21652165
return this.signTransactionTssExternalSignerECDSA(this.baseCoin, params);
21662166
}
21672167

2168+
if (
2169+
_.isFunction(params.customEddsaMPCv2SigningRound1GenerationFunction) &&
2170+
_.isFunction(params.customEddsaMPCv2SigningRound2GenerationFunction) &&
2171+
_.isFunction(params.customEddsaMPCv2SigningRound3GenerationFunction)
2172+
) {
2173+
// invoke external signer TSS for EdDSA MPCv2 workflow
2174+
return this.signTransactionTssExternalSignerEdDSAMPCv2(this.baseCoin, params);
2175+
}
2176+
21682177
if (
21692178
_.isFunction(params.customMPCv2SigningRound1GenerationFunction) &&
21702179
_.isFunction(params.customMPCv2SigningRound2GenerationFunction) &&
@@ -4335,6 +4344,59 @@ export class Wallet implements IWallet {
43354344
}
43364345
}
43374346

4347+
/**
4348+
* Signs a transaction from a TSS EdDSA MPCv2 wallet using external signer.
4349+
*
4350+
* @param params signing options
4351+
*/
4352+
private async signTransactionTssExternalSignerEdDSAMPCv2(
4353+
coin: IBaseCoin,
4354+
params: WalletSignTransactionOptions = {}
4355+
): Promise<TxRequest> {
4356+
let txRequestId = '';
4357+
if (params.txRequestId) {
4358+
txRequestId = params.txRequestId;
4359+
} else if (params.txPrebuild && params.txPrebuild.txRequestId) {
4360+
txRequestId = params.txPrebuild.txRequestId;
4361+
} else {
4362+
throw new Error('TxRequestId required to sign TSS transactions with External Signer.');
4363+
}
4364+
4365+
if (!params.customEddsaMPCv2SigningRound1GenerationFunction) {
4366+
throw new Error(
4367+
'Generator function for EdDSA MPCv2 Round 1 share required to sign transactions with External Signer.'
4368+
);
4369+
}
4370+
4371+
if (!params.customEddsaMPCv2SigningRound2GenerationFunction) {
4372+
throw new Error(
4373+
'Generator function for EdDSA MPCv2 Round 2 share required to sign transactions with External Signer.'
4374+
);
4375+
}
4376+
4377+
if (!params.customEddsaMPCv2SigningRound3GenerationFunction) {
4378+
throw new Error(
4379+
'Generator function for EdDSA MPCv2 Round 3 share required to sign transactions with External Signer.'
4380+
);
4381+
}
4382+
4383+
try {
4384+
assert(this.tssUtils, 'tssUtils must be defined');
4385+
const signedTxRequest = await this.tssUtils.signEddsaMPCv2TssUsingExternalSigner(
4386+
{
4387+
txRequest: txRequestId,
4388+
reqId: params.reqId || new RequestTracer(),
4389+
},
4390+
params.customEddsaMPCv2SigningRound1GenerationFunction,
4391+
params.customEddsaMPCv2SigningRound2GenerationFunction,
4392+
params.customEddsaMPCv2SigningRound3GenerationFunction
4393+
);
4394+
return signedTxRequest;
4395+
} catch (e) {
4396+
throw new Error('failed to sign transaction ' + e);
4397+
}
4398+
}
4399+
43384400
/**
43394401
* Signs a transaction from a TSS ECDSA wallet using external signer.
43404402
*

0 commit comments

Comments
 (0)