Skip to content

Commit 1adcbfc

Browse files
committed
chore(mbe): add mpcv2 signing integration tests
Ticket: WP-5152
1 parent e0ac525 commit 1adcbfc

File tree

4 files changed

+281
-34
lines changed

4 files changed

+281
-34
lines changed
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
import 'should';
2+
import nock from 'nock';
3+
import * as sinon from 'sinon';
4+
import {
5+
BitGoBase,
6+
Wallet,
7+
TxRequest,
8+
IRequestTracer,
9+
TxRequestVersion,
10+
Environments,
11+
RequestTracer,
12+
EcdsaMPCv2Utils,
13+
openpgpUtils,
14+
SignatureShareRecord,
15+
SignatureShareType,
16+
TransactionState,
17+
} from '@bitgo/sdk-core';
18+
import { EnclavedExpressClient } from '../../../../src/api/master/clients/enclavedExpressClient';
19+
import { handleEcdsaSigning } from '../../../../src/api/master/handlers/ecdsa';
20+
import { BitGo } from 'bitgo';
21+
import { readKey } from 'openpgp';
22+
23+
describe('Ecdsa Signing Handler', () => {
24+
let bitgo: BitGoBase;
25+
let wallet: Wallet;
26+
let enclavedExpressClient: EnclavedExpressClient;
27+
let reqId: IRequestTracer;
28+
const bitgoApiUrl = Environments.local.uri;
29+
const enclavedExpressUrl = 'http://enclaved.invalid';
30+
const coin = 'hteth'; // Use hteth for ECDSA testing
31+
const walletId = 'test-wallet-id';
32+
33+
before(() => {
34+
// Disable all real network connections
35+
nock.disableNetConnect();
36+
});
37+
38+
beforeEach(() => {
39+
bitgo = new BitGo({ env: 'local' });
40+
wallet = {
41+
id: () => 'test-wallet-id',
42+
baseCoin: {
43+
getMPCAlgorithm: () => 'ecdsa',
44+
},
45+
multisigTypeVersion: () => 2,
46+
} as unknown as Wallet;
47+
enclavedExpressClient = new EnclavedExpressClient(
48+
{
49+
enclavedExpressUrl,
50+
enclavedExpressCert: 'dummy-cert',
51+
tlsMode: 'disabled',
52+
allowSelfSigned: true,
53+
} as any,
54+
coin,
55+
);
56+
reqId = new RequestTracer();
57+
});
58+
59+
afterEach(() => {
60+
nock.cleanAll();
61+
sinon.restore();
62+
});
63+
64+
after(() => {
65+
// Re-enable network connections after tests
66+
nock.enableNetConnect();
67+
});
68+
69+
it('should successfully sign an ECDSA MPCv2 transaction', async () => {
70+
const txRequest: TxRequest = {
71+
txRequestId: 'test-tx-request-id',
72+
apiVersion: '2.0.0' as TxRequestVersion,
73+
enterpriseId: 'test-enterprise-id',
74+
transactions: [],
75+
state: 'pendingUserSignature',
76+
walletId: 'test-wallet-id',
77+
walletType: 'hot',
78+
version: 2,
79+
date: new Date().toISOString(),
80+
userId: 'test-user-id',
81+
intent: {},
82+
policiesChecked: true,
83+
unsignedTxs: [],
84+
latest: true,
85+
};
86+
const userPubKey = 'test-user-pub-key';
87+
88+
const bitgoGpgKey = await openpgpUtils.generateGPGKeyPair('secp256k1');
89+
const pgpKey = await readKey({ armoredKey: bitgoGpgKey.publicKey });
90+
sinon.stub(EcdsaMPCv2Utils.prototype, 'getBitgoMpcv2PublicGpgKey').resolves(pgpKey);
91+
92+
// Mock getTxRequest call
93+
const getTxRequestNock = nock(bitgoApiUrl)
94+
.get(`/api/v2/wallet/${walletId}/txrequests`)
95+
.query({ txRequestIds: 'test-tx-request-id', latest: true })
96+
.matchHeader('any', () => true)
97+
.reply(200, {
98+
txRequests: [txRequest],
99+
});
100+
101+
// Mock sendSignatureShareV2 calls for each round
102+
const round1SignatureShare: SignatureShareRecord = {
103+
from: SignatureShareType.USER,
104+
to: SignatureShareType.BITGO,
105+
share: JSON.stringify({
106+
type: 'round1Input',
107+
data: {
108+
msg1: {
109+
from: 1,
110+
message: 'round1-message',
111+
},
112+
},
113+
}),
114+
};
115+
116+
const round1TxRequest: TxRequest = {
117+
...txRequest,
118+
transactions: [
119+
{
120+
unsignedTx: {
121+
derivationPath: 'm/0',
122+
signableHex: 'testMessage',
123+
serializedTxHex: 'testMessage',
124+
},
125+
signatureShares: [round1SignatureShare],
126+
state: 'pendingSignature' as TransactionState,
127+
},
128+
],
129+
};
130+
131+
const sendSignatureShareV2Round1Nock = nock(bitgoApiUrl)
132+
.post(`/api/v2/wallet/${walletId}/txrequests/test-tx-request-id/transactions/0/sign`)
133+
.matchHeader('any', () => true)
134+
.reply(200, {
135+
txRequest: round1TxRequest,
136+
});
137+
138+
const round2SignatureShare: SignatureShareRecord = {
139+
from: SignatureShareType.USER,
140+
to: SignatureShareType.BITGO,
141+
share: JSON.stringify({
142+
type: 'round2Input',
143+
data: {
144+
msg2: {
145+
from: 1,
146+
to: 3,
147+
encryptedMessage: 'round2-encrypted-message',
148+
signature: 'round2-signature',
149+
},
150+
msg3: {
151+
from: 1,
152+
to: 3,
153+
encryptedMessage: 'round3-encrypted-message',
154+
signature: 'round3-signature',
155+
},
156+
},
157+
}),
158+
};
159+
160+
const round2TxRequest: TxRequest = {
161+
...round1TxRequest,
162+
transactions: [
163+
{
164+
...round1TxRequest.transactions![0],
165+
signatureShares: [round1SignatureShare, round2SignatureShare],
166+
},
167+
],
168+
};
169+
170+
const sendSignatureShareV2Round2Nock = nock(bitgoApiUrl)
171+
.post(`/api/v2/wallet/${walletId}/txrequests/test-tx-request-id/transactions/0/sign`)
172+
.matchHeader('any', () => true)
173+
.reply(200, {
174+
txRequest: round2TxRequest,
175+
});
176+
177+
const round3SignatureShare: SignatureShareRecord = {
178+
from: SignatureShareType.USER,
179+
to: SignatureShareType.BITGO,
180+
share: JSON.stringify({
181+
type: 'round3Input',
182+
data: {
183+
msg4: {
184+
from: 1,
185+
message: 'round4-message',
186+
signature: 'round4-signature',
187+
signatureR: 'round4-signature-r',
188+
},
189+
},
190+
}),
191+
};
192+
193+
const sendSignatureShareV2Round3Nock = nock(bitgoApiUrl)
194+
.post(`/api/v2/wallet/${walletId}/txrequests/test-tx-request-id/transactions/0/sign`)
195+
.matchHeader('any', () => true)
196+
.reply(200, {
197+
txRequest: {
198+
...round2TxRequest,
199+
transactions: [
200+
{
201+
...round2TxRequest.transactions![0],
202+
signatureShares: [round1SignatureShare, round2SignatureShare, round3SignatureShare],
203+
},
204+
],
205+
},
206+
});
207+
208+
// Mock sendTxRequest call
209+
const sendTxRequestNock = nock(bitgoApiUrl)
210+
.post(`/api/v2/wallet/${walletId}/txrequests/test-tx-request-id/transactions/0/send`)
211+
.matchHeader('any', () => true)
212+
.reply(200, {
213+
...txRequest,
214+
state: 'signed',
215+
});
216+
217+
// Mock MPCv2 Round 1 signing
218+
const signMpcV2Round1NockEbe = nock(enclavedExpressUrl)
219+
.post(`/api/${coin}/mpc/sign/mpcv2round1`)
220+
.reply(200, {
221+
signatureShareRound1: round1SignatureShare,
222+
userGpgPubKey: bitgoGpgKey.publicKey,
223+
encryptedRound1Session: 'encrypted-round1-session',
224+
encryptedUserGpgPrvKey: 'encrypted-user-gpg-prv-key',
225+
encryptedDataKey: 'test-encrypted-data-key',
226+
});
227+
228+
// Mock MPCv2 Round 2 signing
229+
const signMpcV2Round2NockEbe = nock(enclavedExpressUrl)
230+
.post(`/api/${coin}/mpc/sign/mpcv2round2`)
231+
.reply(200, {
232+
signatureShareRound2: round2SignatureShare,
233+
encryptedRound2Session: 'encrypted-round2-session',
234+
});
235+
236+
// Mock MPCv2 Round 3 signing
237+
const signMpcV2Round3NockEbe = nock(enclavedExpressUrl)
238+
.post(`/api/${coin}/mpc/sign/mpcv2round3`)
239+
.reply(200, {
240+
signatureShareRound3: round3SignatureShare,
241+
});
242+
243+
const result = await handleEcdsaSigning(
244+
bitgo,
245+
wallet,
246+
txRequest.txRequestId,
247+
enclavedExpressClient,
248+
'user',
249+
userPubKey,
250+
reqId,
251+
);
252+
253+
result.should.eql({
254+
...txRequest,
255+
state: 'signed',
256+
});
257+
258+
getTxRequestNock.done();
259+
sendSignatureShareV2Round1Nock.done();
260+
sendSignatureShareV2Round2Nock.done();
261+
sendSignatureShareV2Round3Nock.done();
262+
sendTxRequestNock.done();
263+
signMpcV2Round1NockEbe.done();
264+
signMpcV2Round2NockEbe.done();
265+
signMpcV2Round3NockEbe.done();
266+
});
267+
});

src/api/enclaved/handlers/signMpcTransaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ async function handleEcdsaMpcV2Signing(
237237

238238
switch (shareType.toLowerCase()) {
239239
case ShareType.MPCv2Round1: {
240-
const dataKey = await generateDataKey({ keyType: 'RSA-2048', cfg });
240+
const dataKey = await generateDataKey({ keyType: 'AES-256', cfg });
241241
return {
242242
...(await ecdsaMPCv2Utils.createOfflineRound1Share({
243243
txRequest: params.txRequest,

src/api/master/handlers/ecdsa.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,28 @@ import {
22
BitGoBase,
33
getTxRequest,
44
Wallet,
5-
TxRequest,
65
IRequestTracer,
76
EcdsaMPCv2Utils,
87
commonTssMethods,
98
RequestType,
109
} from '@bitgo/sdk-core';
1110
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
1211
import logger from '../../../logger';
13-
import { sendTxRequest } from '@bitgo/sdk-core/dist/src/bitgo/tss/common';
1412

1513
export async function handleEcdsaSigning(
1614
bitgo: BitGoBase,
1715
wallet: Wallet,
18-
txRequest: string | TxRequest,
16+
txRequestId: string,
1917
enclavedExpressClient: EnclavedExpressClient,
2018
source: 'user' | 'backup',
2119
commonKeychain: string,
2220
reqId?: IRequestTracer,
2321
) {
24-
let txRequestResolved: TxRequest;
25-
let txRequestId: string;
2622
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-
}
23+
const txRequest = await getTxRequest(bitgo, wallet.id(), txRequestId, reqId);
3524

3625
// Get BitGo GPG key for MPCv2
37-
const bitgoGpgKey = await ecdsaMPCv2Utils.getBitgoPublicGpgKey();
26+
const bitgoGpgKey = await ecdsaMPCv2Utils.getBitgoMpcv2PublicGpgKey();
3827

3928
// Round 1: Generate user's Round 1 share
4029
const {
@@ -44,7 +33,7 @@ export async function handleEcdsaSigning(
4433
encryptedUserGpgPrvKey,
4534
encryptedDataKey,
4635
} = await enclavedExpressClient.signMpcV2Round1({
47-
txRequest: txRequestResolved,
36+
txRequest,
4837
bitgoGpgPubKey: bitgoGpgKey.armor(),
4938
source,
5039
pub: commonKeychain,
@@ -116,10 +105,10 @@ export async function handleEcdsaSigning(
116105
);
117106

118107
logger.debug('Successfully completed ECDSA MPCv2 signing!');
119-
return sendTxRequest(
108+
return commonTssMethods.sendTxRequest(
120109
bitgo,
121-
txRequestResolved.walletId,
122-
txRequestResolved.txRequestId,
110+
txRequest.walletId,
111+
txRequest.txRequestId,
123112
RequestType.tx,
124113
reqId,
125114
);

0 commit comments

Comments
 (0)