Skip to content

Commit 4dfab42

Browse files
authored
Merge pull request #66 from BitGo/WP-5246-custom-signing-fns-sendmany
feat(mbe): use customSigning fns for MPC eddsa
2 parents 15c27bd + b56e3da commit 4dfab42

File tree

5 files changed

+156
-200
lines changed

5 files changed

+156
-200
lines changed

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

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { BitGo } from 'bitgo';
1717
import { readKey } from 'openpgp';
1818

1919
// TODO: Re-enable once using EDDSA Custom signing fns
20-
xdescribe('Eddsa Signing Handler', () => {
20+
describe('Eddsa Signing Handler', () => {
2121
let bitgo: BitGoBase;
2222
let wallet: Wallet;
2323
let enclavedExpressClient: EnclavedExpressClient;
@@ -105,30 +105,6 @@ xdescribe('Eddsa Signing Handler', () => {
105105
const pgpKey = await readKey({ armoredKey: bitgoGpgKey.publicKey });
106106
sinon.stub(EddsaUtils.prototype, 'getBitgoPublicGpgKey').resolves(pgpKey);
107107

108-
// Mock getTxRequest call
109-
const getTxRequestNock = nock(bitgoApiUrl)
110-
.get(`/api/v2/wallet/${walletId}/txrequests`)
111-
.query({ txRequestIds: 'test-tx-request-id', latest: true })
112-
.matchHeader('any', () => true)
113-
.reply(200, {
114-
txRequests: [
115-
{
116-
txRequestId: 'test-tx-request-id',
117-
state: 'signed',
118-
apiVersion: 'full',
119-
pendingApprovalId: 'test-pending-approval-id',
120-
transactions: [
121-
{
122-
unsignedTx: {
123-
derivationPath: 'm/0',
124-
signableHex: 'testMessage',
125-
},
126-
},
127-
],
128-
},
129-
],
130-
});
131-
132108
// Mock exchangeEddsaCommitments call
133109
const exchangeCommitmentsNock = nock(bitgoApiUrl)
134110
.post(`/api/v2/wallet/${walletId}/txrequests/test-tx-request-id/transactions/0/commit`)
@@ -266,7 +242,6 @@ xdescribe('Eddsa Signing Handler', () => {
266242
state: 'signed',
267243
});
268244

269-
getTxRequestNock.done();
270245
exchangeCommitmentsNock.done();
271246
offerRShareNock.done();
272247
getBitgoRShareNock.done();

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

Lines changed: 25 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
88
import { Environments, Wallet } from '@bitgo/sdk-core';
99
import { Coin } from 'bitgo';
1010
import assert from 'assert';
11-
import * as eddsa from '../../../api/master/handlers/eddsa';
1211

1312
describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
1413
let agent: request.SuperAgentTest;
@@ -228,25 +227,6 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
228227
describe('SendMany TSS EDDSA:', () => {
229228
const coin = 'tsol';
230229
it('should send many transactions using EDDSA TSS signing', async () => {
231-
const mockTxRequest = {
232-
txRequestId: 'test-tx-request-id',
233-
state: 'signed',
234-
apiVersion: 'full',
235-
pendingApprovalId: 'test-pending-approval-id',
236-
transactions: [
237-
{
238-
unsignedTx: {
239-
derivationPath: 'm/0',
240-
signableHex: 'testMessage',
241-
},
242-
signedTx: {
243-
id: 'test-tx-id',
244-
tx: 'signed-transaction',
245-
},
246-
},
247-
],
248-
};
249-
250230
// Mock wallet get request for TSS wallet
251231
const walletGetNock = nock(bitgoApiUrl)
252232
.get(`/api/v2/${coin}/wallet/${walletId}`)
@@ -270,40 +250,35 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
270250
source: 'user',
271251
type: 'tss',
272252
});
273-
274-
const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({
275-
txRequestId: 'test-tx-request-id',
276-
txHex: 'prebuilt-tx-hex',
277-
txInfo: {
278-
nP2SHInputs: 1,
279-
nSegwitInputs: 0,
280-
nOutputs: 2,
253+
const sendManyStub = sinon.stub(Wallet.prototype, 'sendMany').resolves({
254+
txRequest: {
255+
txRequestId: 'test-tx-request-id',
256+
state: 'signed',
257+
apiVersion: 'full',
258+
pendingApprovalId: 'test-pending-approval-id',
259+
transactions: [
260+
{
261+
state: 'signed',
262+
unsignedTx: {
263+
derivationPath: 'm/0',
264+
signableHex: 'testMessage',
265+
serializedTxHex: 'testSerializedTxHex',
266+
},
267+
signatureShares: [],
268+
signedTx: {
269+
id: 'test-tx-id',
270+
tx: 'signed-transaction',
271+
},
272+
},
273+
],
281274
},
282-
walletId,
275+
txid: 'test-tx-id',
276+
tx: 'signed-transaction',
283277
});
284278

285-
const verifyStub = sinon.stub(Coin.Tsol.prototype, 'verifyTransaction').resolves(true);
286-
287279
// Mock multisigType to return 'tss'
288280
const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss');
289281

290-
// Mock handleEddsaSigning
291-
const handleEddsaSigningStub = sinon.stub().resolves({
292-
...mockTxRequest,
293-
});
294-
295-
// Import and stub the signAndSendTxRequests function
296-
sinon.stub(eddsa, 'handleEddsaSigning').callsFake(handleEddsaSigningStub);
297-
298-
// Mock getTxRequest call
299-
const getTxRequestNock = nock(bitgoApiUrl)
300-
.get(`/api/v2/wallet/${walletId}/txrequests`)
301-
.query({ txRequestIds: 'test-tx-request-id', latest: true })
302-
.matchHeader('any', () => true)
303-
.reply(200, {
304-
txRequests: [mockTxRequest],
305-
});
306-
307282
const response = await agent
308283
.post(`/api/${coin}/wallet/${walletId}/sendMany`)
309284
.set('Authorization', `Bearer ${accessToken}`)
@@ -329,11 +304,8 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => {
329304

330305
walletGetNock.done();
331306
keychainGetNock.done();
332-
sinon.assert.calledOnce(prebuildStub);
333-
sinon.assert.calledOnce(verifyStub);
334-
sinon.assert.calledThrice(multisigTypeStub);
335-
sinon.assert.calledOnce(handleEddsaSigningStub);
336-
getTxRequestNock.done();
307+
sinon.assert.calledOnce(sendManyStub);
308+
sinon.assert.calledOnce(multisigTypeStub);
337309
});
338310
});
339311

src/api/master/clients/enclavedExpressClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ interface SignMpcCommitmentParams {
9595
pub: string;
9696
}
9797

98-
interface SignMpcCommitmentResponse {
98+
export interface SignMpcCommitmentResponse {
9999
userToBitgoCommitment: CommitmentShareRecord;
100100
encryptedSignerShare: EncryptedSignerShareRecord;
101101
encryptedUserToBitgoRShare: EncryptedSignerShareRecord;

src/api/master/handlers/eddsa.ts

Lines changed: 91 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,97 @@
11
import {
22
BitGoBase,
3-
getTxRequest,
4-
offerUserToBitgoRShare,
5-
getBitgoToUserRShare,
6-
sendUserToBitgoGShare,
73
Wallet,
84
IRequestTracer,
95
EddsaUtils,
106
BaseCoin,
117
ApiKeyShare,
128
TxRequest,
9+
CommitmentShareRecord,
10+
EncryptedSignerShareRecord,
11+
SignShare,
12+
SignatureShareRecord,
13+
CustomCommitmentGeneratingFunction,
14+
CustomRShareGeneratingFunction,
15+
CustomGShareGeneratingFunction,
1316
} from '@bitgo/sdk-core';
14-
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
15-
import { exchangeEddsaCommitments } from '@bitgo/sdk-core/dist/src/bitgo/tss/common';
16-
import logger from '../../../logger';
17+
import { EnclavedExpressClient, SignMpcCommitmentResponse } from '../clients/enclavedExpressClient';
18+
19+
/**
20+
* Creates custom EdDSA signing functions for use with enclaved express client
21+
*/
22+
export function createEddsaCustomSigningFunctions(
23+
enclavedExpressClient: EnclavedExpressClient,
24+
source: 'user' | 'backup',
25+
commonKeychain: string,
26+
): {
27+
customCommitmentGenerator: CustomCommitmentGeneratingFunction;
28+
customRShareGenerator: CustomRShareGeneratingFunction;
29+
customGShareGenerator: CustomGShareGeneratingFunction;
30+
} {
31+
// Create state to maintain data between rounds
32+
let commitmentResponse: SignMpcCommitmentResponse;
33+
34+
// Create custom signing methods that maintain state
35+
const customCommitmentGenerator: CustomCommitmentGeneratingFunction = async (params: {
36+
txRequest: TxRequest;
37+
bitgoGpgPubKey?: string;
38+
}) => {
39+
if (!params.bitgoGpgPubKey) {
40+
throw new Error('bitgoGpgPubKey is required for commitment share generation');
41+
}
42+
const response = await enclavedExpressClient.signMpcCommitment({
43+
txRequest: params.txRequest,
44+
bitgoPublicGpgKey: params.bitgoGpgPubKey,
45+
source,
46+
pub: commonKeychain,
47+
});
48+
commitmentResponse = response;
49+
return response;
50+
};
51+
52+
const customRShareGenerator: CustomRShareGeneratingFunction = async (params: {
53+
txRequest: TxRequest;
54+
encryptedUserToBitgoRShare: EncryptedSignerShareRecord;
55+
}) => {
56+
if (!commitmentResponse) {
57+
throw new Error('Commitment must be completed before R-share generation');
58+
}
59+
const response = await enclavedExpressClient.signMpcRShare({
60+
txRequest: params.txRequest,
61+
encryptedUserToBitgoRShare: params.encryptedUserToBitgoRShare,
62+
encryptedDataKey: commitmentResponse.encryptedDataKey,
63+
source,
64+
pub: commonKeychain,
65+
});
66+
return { rShare: response.rShare };
67+
};
68+
69+
const customGShareGenerator: CustomGShareGeneratingFunction = async (params: {
70+
txRequest: TxRequest;
71+
userToBitgoRShare: SignShare;
72+
bitgoToUserRShare: SignatureShareRecord;
73+
bitgoToUserCommitment: CommitmentShareRecord;
74+
}) => {
75+
if (!commitmentResponse) {
76+
throw new Error('Commitment must be completed before G-share generation');
77+
}
78+
const response = await enclavedExpressClient.signMpcGShare({
79+
txRequest: params.txRequest,
80+
bitgoToUserRShare: params.bitgoToUserRShare,
81+
userToBitgoRShare: params.userToBitgoRShare,
82+
bitgoToUserCommitment: params.bitgoToUserCommitment,
83+
source,
84+
pub: commonKeychain,
85+
});
86+
return response.gShare;
87+
};
88+
89+
return {
90+
customCommitmentGenerator,
91+
customRShareGenerator,
92+
customGShareGenerator,
93+
};
94+
}
1795

1896
export async function handleEddsaSigning(
1997
bitgo: BitGoBase,
@@ -24,70 +102,15 @@ export async function handleEddsaSigning(
24102
reqId?: IRequestTracer,
25103
) {
26104
const eddsaUtils = new EddsaUtils(bitgo, wallet.baseCoin, wallet);
27-
28-
const { apiVersion } = txRequest;
29-
const bitgoGpgKey = await eddsaUtils.getBitgoPublicGpgKey();
30-
31-
const {
32-
userToBitgoCommitment,
33-
encryptedSignerShare,
34-
encryptedUserToBitgoRShare,
35-
encryptedDataKey,
36-
} = await enclavedExpressClient.signMpcCommitment({
105+
const { customCommitmentGenerator, customRShareGenerator, customGShareGenerator } =
106+
createEddsaCustomSigningFunctions(enclavedExpressClient, 'user', commonKeychain);
107+
return await eddsaUtils.signEddsaTssUsingExternalSigner(
37108
txRequest,
38-
bitgoPublicGpgKey: bitgoGpgKey.armor(),
39-
source: 'user',
40-
pub: commonKeychain,
41-
});
42-
43-
const { commitmentShare: bitgoToUserCommitment } = await exchangeEddsaCommitments(
44-
bitgo,
45-
wallet.id(),
46-
txRequest.txRequestId,
47-
userToBitgoCommitment,
48-
encryptedSignerShare,
49-
apiVersion,
50-
reqId,
51-
);
52-
53-
const { rShare } = await enclavedExpressClient.signMpcRShare({
54-
txRequest,
55-
encryptedUserToBitgoRShare,
56-
encryptedDataKey,
57-
source: 'user',
58-
pub: commonKeychain,
59-
});
60-
61-
await offerUserToBitgoRShare(
62-
bitgo,
63-
wallet.id(),
64-
txRequest.txRequestId,
65-
rShare,
66-
encryptedSignerShare.share,
67-
apiVersion,
109+
customCommitmentGenerator,
110+
customRShareGenerator,
111+
customGShareGenerator,
68112
reqId,
69113
);
70-
const bitgoToUserRShare = await getBitgoToUserRShare(
71-
bitgo,
72-
wallet.id(),
73-
txRequest.txRequestId,
74-
reqId,
75-
);
76-
const gSignShareTransactionParams = {
77-
txRequest,
78-
bitgoToUserRShare: bitgoToUserRShare,
79-
userToBitgoRShare: rShare,
80-
bitgoToUserCommitment,
81-
};
82-
const { gShare } = await enclavedExpressClient.signMpcGShare({
83-
...gSignShareTransactionParams,
84-
source: 'user',
85-
pub: commonKeychain,
86-
});
87-
88-
await sendUserToBitgoGShare(bitgo, wallet.id(), txRequest.txRequestId, gShare, apiVersion, reqId);
89-
logger.debug('Successfully completed signing!');
90-
return await getTxRequest(bitgo, wallet.id(), txRequest.txRequestId, reqId);
91114
}
92115

93116
interface OrchestrateEddsaKeyGenParams {

0 commit comments

Comments
 (0)