Skip to content

Commit a0bc5cd

Browse files
committed
feat(mbe): add mpcv2 signing support for sendmany
Ticket: WP-5152
1 parent 713f293 commit a0bc5cd

4 files changed

Lines changed: 258 additions & 2 deletions

File tree

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

Whitespace-only changes.

src/api/master/clients/enclavedExpressClient.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,51 @@ interface SignMpcGShareResponse {
124124
gShare: GShare;
125125
}
126126

127+
// ECDSA MPCv2 interfaces
128+
interface SignMpcV2Round1Params {
129+
txRequest: TxRequest;
130+
bitgoGpgPubKey: string;
131+
source: 'user' | 'backup';
132+
pub: string;
133+
}
134+
135+
interface SignMpcV2Round1Response {
136+
signatureShareRound1: SignatureShareRecord;
137+
userGpgPubKey: string;
138+
encryptedRound1Session: string;
139+
encryptedUserGpgPrvKey: string;
140+
encryptedDataKey: string;
141+
}
142+
143+
interface SignMpcV2Round2Params {
144+
txRequest: TxRequest;
145+
bitgoGpgPubKey: string;
146+
encryptedDataKey: string;
147+
encryptedUserGpgPrvKey: string;
148+
encryptedRound1Session: string;
149+
source: 'user' | 'backup';
150+
pub: string;
151+
}
152+
153+
interface SignMpcV2Round2Response {
154+
signatureShareRound2: SignatureShareRecord;
155+
encryptedRound2Session: string;
156+
}
157+
158+
interface SignMpcV2Round3Params {
159+
txRequest: TxRequest;
160+
bitgoGpgPubKey: string;
161+
encryptedDataKey: string;
162+
encryptedUserGpgPrvKey: string;
163+
encryptedRound2Session: string;
164+
source: 'user' | 'backup';
165+
pub: string;
166+
}
167+
168+
interface SignMpcV2Round3Response {
169+
signatureShareRound3: SignatureShareRecord;
170+
}
171+
127172
export class EnclavedExpressClient {
128173
private readonly baseUrl: string;
129174
private readonly enclavedExpressCert: string;
@@ -456,6 +501,78 @@ export class EnclavedExpressClient {
456501
throw err;
457502
}
458503
}
504+
505+
async signMpcV2Round1(params: SignMpcV2Round1Params): Promise<SignMpcV2Round1Response> {
506+
if (!this.coin) {
507+
throw new Error('Coin must be specified to sign an MPCv2 Round 1');
508+
}
509+
510+
try {
511+
let request = this.apiClient['v1.mpc.sign'].post({
512+
coin: this.coin,
513+
shareType: 'mpcv2round1',
514+
...params,
515+
});
516+
517+
if (this.tlsMode === TlsMode.MTLS) {
518+
request = request.agent(this.createHttpsAgent());
519+
}
520+
const response = await request.decodeExpecting(200);
521+
return response.body;
522+
} catch (error) {
523+
const err = error as Error;
524+
debugLogger('Failed to sign mpcv2 round 1: %s', err.message);
525+
throw err;
526+
}
527+
}
528+
529+
async signMpcV2Round2(params: SignMpcV2Round2Params): Promise<SignMpcV2Round2Response> {
530+
if (!this.coin) {
531+
throw new Error('Coin must be specified to sign an MPCv2 Round 2');
532+
}
533+
534+
try {
535+
let request = this.apiClient['v1.mpc.sign'].post({
536+
coin: this.coin,
537+
shareType: 'mpcv2round2',
538+
...params,
539+
});
540+
541+
if (this.tlsMode === TlsMode.MTLS) {
542+
request = request.agent(this.createHttpsAgent());
543+
}
544+
const response = await request.decodeExpecting(200);
545+
return response.body;
546+
} catch (error) {
547+
const err = error as Error;
548+
debugLogger('Failed to sign mpcv2 round 2: %s', err.message);
549+
throw err;
550+
}
551+
}
552+
553+
async signMpcV2Round3(params: SignMpcV2Round3Params): Promise<SignMpcV2Round3Response> {
554+
if (!this.coin) {
555+
throw new Error('Coin must be specified to sign an MPCv2 Round 3');
556+
}
557+
558+
try {
559+
let request = this.apiClient['v1.mpc.sign'].post({
560+
coin: this.coin,
561+
shareType: 'mpcv2round3',
562+
...params,
563+
});
564+
565+
if (this.tlsMode === TlsMode.MTLS) {
566+
request = request.agent(this.createHttpsAgent());
567+
}
568+
const response = await request.decodeExpecting(200);
569+
return response.body;
570+
} catch (error) {
571+
const err = error as Error;
572+
debugLogger('Failed to sign mpcv2 round 3: %s', err.message);
573+
throw err;
574+
}
575+
}
459576
}
460577

461578
/**

src/api/master/handlers/ecdsa.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import {
2+
BitGoBase,
3+
getTxRequest,
4+
Wallet,
5+
TxRequest,
6+
IRequestTracer,
7+
EcdsaMPCv2Utils,
8+
commonTssMethods,
9+
RequestType,
10+
} from '@bitgo/sdk-core';
11+
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
12+
import logger from '../../../logger';
13+
import { sendTxRequest } from '@bitgo/sdk-core/dist/src/bitgo/tss/common';
14+
15+
export async function handleEcdsaSigning(
16+
bitgo: BitGoBase,
17+
wallet: Wallet,
18+
txRequest: string | TxRequest,
19+
enclavedExpressClient: EnclavedExpressClient,
20+
source: 'user' | 'backup',
21+
commonKeychain: string,
22+
reqId?: IRequestTracer,
23+
) {
24+
let txRequestResolved: TxRequest;
25+
let txRequestId: string;
26+
const ecdsaMPCv2Utils = new EcdsaMPCv2Utils(bitgo, wallet.baseCoin);
27+
28+
if (typeof txRequest === 'string') {
29+
txRequestResolved = await getTxRequest(bitgo, wallet.id(), txRequest, reqId);
30+
txRequestId = txRequestResolved.txRequestId;
31+
} else {
32+
txRequestResolved = txRequest;
33+
txRequestId = txRequest.txRequestId;
34+
}
35+
36+
// Get BitGo GPG key for MPCv2
37+
const bitgoGpgKey = await ecdsaMPCv2Utils.getBitgoPublicGpgKey();
38+
39+
// Round 1: Generate user's Round 1 share
40+
const {
41+
signatureShareRound1,
42+
userGpgPubKey,
43+
encryptedRound1Session,
44+
encryptedUserGpgPrvKey,
45+
encryptedDataKey,
46+
} = await enclavedExpressClient.signMpcV2Round1({
47+
txRequest: txRequestResolved,
48+
bitgoGpgPubKey: bitgoGpgKey.armor(),
49+
source,
50+
pub: commonKeychain,
51+
});
52+
53+
// Send Round 1 share to BitGo and get updated txRequest
54+
const round1TxRequest = await commonTssMethods.sendSignatureShareV2(
55+
bitgo,
56+
wallet.id(),
57+
txRequestId,
58+
[signatureShareRound1],
59+
RequestType.tx,
60+
wallet.baseCoin.getMPCAlgorithm(),
61+
userGpgPubKey,
62+
undefined,
63+
wallet.multisigTypeVersion(),
64+
reqId,
65+
);
66+
67+
// Round 2: Generate user's Round 2 share
68+
const { signatureShareRound2, encryptedRound2Session } =
69+
await enclavedExpressClient.signMpcV2Round2({
70+
txRequest: round1TxRequest,
71+
bitgoGpgPubKey: bitgoGpgKey.armor(),
72+
encryptedDataKey,
73+
encryptedUserGpgPrvKey,
74+
encryptedRound1Session,
75+
source,
76+
pub: commonKeychain,
77+
});
78+
79+
// Send Round 2 share to BitGo and get updated txRequest
80+
const round2TxRequest = await commonTssMethods.sendSignatureShareV2(
81+
bitgo,
82+
wallet.id(),
83+
txRequestId,
84+
[signatureShareRound2],
85+
RequestType.tx,
86+
wallet.baseCoin.getMPCAlgorithm(),
87+
userGpgPubKey,
88+
undefined,
89+
wallet.multisigTypeVersion(),
90+
reqId,
91+
);
92+
93+
// Round 3: Generate user's Round 3 share
94+
const { signatureShareRound3 } = await enclavedExpressClient.signMpcV2Round3({
95+
txRequest: round2TxRequest,
96+
bitgoGpgPubKey: bitgoGpgKey.armor(),
97+
encryptedDataKey,
98+
encryptedUserGpgPrvKey,
99+
encryptedRound2Session,
100+
source,
101+
pub: commonKeychain,
102+
});
103+
104+
// Send Round 3 share to BitGo
105+
await commonTssMethods.sendSignatureShareV2(
106+
bitgo,
107+
wallet.id(),
108+
txRequestId,
109+
[signatureShareRound3],
110+
RequestType.tx,
111+
wallet.baseCoin.getMPCAlgorithm(),
112+
userGpgPubKey,
113+
undefined,
114+
wallet.multisigTypeVersion(),
115+
reqId,
116+
);
117+
118+
logger.debug('Successfully completed ECDSA MPCv2 signing!');
119+
return sendTxRequest(
120+
bitgo,
121+
txRequestResolved.walletId,
122+
txRequestResolved.txRequestId,
123+
RequestType.tx,
124+
reqId,
125+
);
126+
}

src/api/master/handlers/handleSendMany.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import logger from '../../../logger';
1515
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
1616
import { handleEddsaSigning } from './eddsa';
17+
import { handleEcdsaSigning } from './ecdsa';
1718
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
1819

1920
/**
@@ -197,7 +198,9 @@ async function signAndSendTxRequests(
197198
}
198199

199200
let signedTxRequest: TxRequest;
200-
if (wallet.baseCoin.getMPCAlgorithm() === 'eddsa') {
201+
const mpcAlgorithm = wallet.baseCoin.getMPCAlgorithm();
202+
203+
if (mpcAlgorithm === 'eddsa') {
201204
signedTxRequest = await handleEddsaSigning(
202205
bitgo,
203206
wallet,
@@ -206,8 +209,18 @@ async function signAndSendTxRequests(
206209
signingKeychain.commonKeychain,
207210
reqId,
208211
);
212+
} else if (mpcAlgorithm === 'ecdsa') {
213+
signedTxRequest = await handleEcdsaSigning(
214+
bitgo,
215+
wallet,
216+
txRequestId,
217+
enclavedExpressClient,
218+
signingKeychain.source as 'user' | 'backup',
219+
signingKeychain.commonKeychain,
220+
reqId,
221+
);
209222
} else {
210-
throw new Error('Unsupported MPC algorithm');
223+
throw new Error(`Unsupported MPC algorithm: ${mpcAlgorithm}`);
211224
}
212225

213226
if (!signedTxRequest.txRequestId) {

0 commit comments

Comments
 (0)