Skip to content

Commit 2a5094f

Browse files
authored
Merge pull request #41 from BitGo/WP-4760-send-many-mpc-eddsa
feat(mbe): add support for mpc eddsa sendmany + signing
2 parents 144b235 + badf21f commit 2a5094f

File tree

11 files changed

+1458
-204
lines changed

11 files changed

+1458
-204
lines changed

masterBitgoExpress.json

Lines changed: 653 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"test:coverage": "nyc mocha --require ts-node/register 'src/**/__tests__/**/*.test.ts'",
1616
"lint": "eslint --quiet .",
1717
"generate-test-ssl": "openssl req -x509 -newkey rsa:2048 -keyout test-ssl-key.pem -out test-ssl-cert.pem -days 365 -nodes -subj '/CN=localhost'",
18-
"generate:openapi:masterExpress": "npx @api-ts/openapi-generator --name @bitgo/master-bitgo-express ./src/masterBitgoExpress/routers/index.ts > masterBitgoExpress.json",
18+
"generate:openapi:masterExpress": "npx @api-ts/openapi-generator --name @bitgo/master-bitgo-express ./src/api/master/routers/index.ts > masterBitgoExpress.json",
1919
"container:build": "podman build -t bitgo-onprem-express ."
2020
},
2121
"dependencies": {
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
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+
EddsaUtils,
13+
openpgpUtils,
14+
} from '@bitgo/sdk-core';
15+
import { EnclavedExpressClient } from '../../../../src/api/master/clients/enclavedExpressClient';
16+
import { handleEddsaSigning } from '../../../../src/api/master/handlers/eddsa';
17+
import { BitGo } from 'bitgo';
18+
import { readKey } from 'openpgp';
19+
20+
describe('Eddsa Signing Handler', () => {
21+
let bitgo: BitGoBase;
22+
let wallet: Wallet;
23+
let enclavedExpressClient: EnclavedExpressClient;
24+
let reqId: IRequestTracer;
25+
const bitgoApiUrl = Environments.local.uri;
26+
const enclavedExpressUrl = 'http://enclaved.invalid';
27+
const coin = 'tbtc';
28+
const walletId = 'test-wallet-id';
29+
30+
before(() => {
31+
// Disable all real network connections
32+
nock.disableNetConnect();
33+
});
34+
35+
beforeEach(() => {
36+
bitgo = new BitGo({ env: 'local' });
37+
wallet = {
38+
id: () => 'test-wallet-id',
39+
} as Wallet;
40+
enclavedExpressClient = new EnclavedExpressClient(
41+
{
42+
enclavedExpressUrl,
43+
enclavedExpressCert: 'dummy-cert',
44+
tlsMode: 'disabled',
45+
allowSelfSigned: true,
46+
} as any,
47+
coin,
48+
);
49+
reqId = new RequestTracer();
50+
});
51+
52+
afterEach(() => {
53+
nock.cleanAll();
54+
sinon.restore();
55+
});
56+
57+
after(() => {
58+
// Re-enable network connections after tests
59+
nock.enableNetConnect();
60+
});
61+
62+
it('should successfully sign an Eddsa transaction', async () => {
63+
const txRequest: TxRequest = {
64+
txRequestId: 'test-tx-request-id',
65+
apiVersion: '2.0.0' as TxRequestVersion,
66+
enterpriseId: 'test-enterprise-id',
67+
transactions: [],
68+
state: 'pendingUserSignature',
69+
walletId: 'test-wallet-id',
70+
walletType: 'hot',
71+
version: 2,
72+
date: new Date().toISOString(),
73+
userId: 'test-user-id',
74+
intent: {},
75+
policiesChecked: true,
76+
unsignedTxs: [],
77+
latest: true,
78+
};
79+
const userPubKey = 'test-user-pub-key';
80+
81+
const bitgoGpgKey = await openpgpUtils.generateGPGKeyPair('ed25519');
82+
const getGPGKeysStub = sinon.stub().resolves([{ pub: bitgoGpgKey.publicKey }]);
83+
84+
const pgpKey = await readKey({ armoredKey: bitgoGpgKey.publicKey });
85+
sinon.stub(EddsaUtils.prototype, 'getBitgoPublicGpgKey').resolves(pgpKey);
86+
87+
// Mock getTxRequest call
88+
const getTxRequestNock = nock(bitgoApiUrl)
89+
.get(`/api/v2/wallet/${walletId}/txrequests`)
90+
.query({ txRequestIds: 'test-tx-request-id', latest: true })
91+
.matchHeader('any', () => true)
92+
.reply(200, {
93+
txRequests: [
94+
{
95+
txRequestId: 'test-tx-request-id',
96+
state: 'signed',
97+
apiVersion: 'full',
98+
pendingApprovalId: 'test-pending-approval-id',
99+
transactions: [
100+
{
101+
unsignedTx: {
102+
derivationPath: 'm/0',
103+
signableHex: 'testMessage',
104+
},
105+
},
106+
],
107+
},
108+
],
109+
});
110+
111+
// Mock exchangeEddsaCommitments call
112+
const exchangeCommitmentsNock = nock(bitgoApiUrl)
113+
.post(`/api/v2/wallet/${walletId}/txrequests/test-tx-request-id/transactions/0/commit`)
114+
.matchHeader('any', () => true)
115+
.reply(200, {
116+
commitmentShare: { share: 'bitgo-commitment-share' },
117+
});
118+
119+
// Mock offerUserToBitgoRShare call
120+
const offerRShareNock = nock(bitgoApiUrl)
121+
.post(
122+
`/api/v2/wallet/${walletId}/txrequests/test-tx-request-id/transactions/0/signatureshares`,
123+
)
124+
.matchHeader('any', () => true)
125+
.reply(200, {
126+
share: 'user-to-bitgo-r-share',
127+
from: 'bitgo',
128+
to: 'user',
129+
});
130+
131+
// Mock getBitgoToUserRShare call
132+
const getBitgoRShareNock = nock(bitgoApiUrl)
133+
.get(`/api/v2/wallet/${walletId}/txrequests`)
134+
.query({ txRequestIds: 'test-tx-request-id', latest: true })
135+
.matchHeader('any', () => true)
136+
.reply(200, {
137+
txRequests: [
138+
{
139+
txRequestId: 'test-tx-request-id',
140+
state: 'signed',
141+
apiVersion: 'full',
142+
pendingApprovalId: 'test-pending-approval-id',
143+
transactions: [
144+
{
145+
unsignedTx: {
146+
derivationPath: 'm/0',
147+
signableHex: 'testMessage',
148+
},
149+
signatureShares: [
150+
{
151+
share: 'bitgo-to-user-r-share',
152+
from: 'bitgo',
153+
to: 'user',
154+
type: 'r',
155+
},
156+
{
157+
share: 'user-to-bitgo-r-share',
158+
from: 'user',
159+
to: 'bitgo',
160+
type: 'r',
161+
},
162+
],
163+
},
164+
],
165+
},
166+
],
167+
});
168+
169+
// Mock sendUserToBitgoGShare call
170+
const sendGShareNock = nock(bitgoApiUrl)
171+
.post(
172+
`/api/v2/wallet/${walletId}/txrequests/test-tx-request-id/transactions/0/signatureshares`,
173+
)
174+
.matchHeader('any', () => true)
175+
.reply(200, {
176+
share: 'user-to-bitgo-g-share',
177+
from: 'bitgo',
178+
to: 'user',
179+
});
180+
181+
// Mock final getTxRequest call
182+
const finalGetTxRequestNock = nock(bitgoApiUrl)
183+
.get(`/api/v2/wallet/${walletId}/txrequests`)
184+
.query({ txRequestIds: 'test-tx-request-id', latest: true })
185+
.matchHeader('any', () => true)
186+
.reply(200, {
187+
txRequests: [
188+
{
189+
...txRequest,
190+
state: 'signed',
191+
},
192+
],
193+
});
194+
195+
// Mock MPC commitment signing
196+
const signMpcCommitmentNockEbe = nock(enclavedExpressUrl)
197+
.post(`/api/${coin}/mpc/sign/commitment`)
198+
.reply(200, {
199+
userToBitgoCommitment: { share: 'user-commitment-share' },
200+
encryptedSignerShare: { share: 'encrypted-signer-share' },
201+
encryptedUserToBitgoRShare: { share: 'encrypted-user-to-bitgo-r-share' },
202+
encryptedDataKey: 'test-encrypted-data-key',
203+
});
204+
205+
// Mock MPC R-share signing
206+
const signMpcRShareNockEbe = nock(enclavedExpressUrl)
207+
.post(`/api/${coin}/mpc/sign/r`)
208+
.reply(200, {
209+
rShare: {
210+
rShares: [
211+
{ r: 'r-share', R: 'R-share' },
212+
{ r: 'r-share-2', R: 'R-share-2' },
213+
{ r: 'r-share-3', R: 'R-share-3' },
214+
{ r: 'r-share-4', R: 'R-share-4', i: 3, j: 1 },
215+
],
216+
},
217+
});
218+
219+
// Mock MPC G-share signing
220+
const signMpcGShareNockEbe = nock(enclavedExpressUrl)
221+
.post(`/api/${coin}/mpc/sign/g`)
222+
.reply(200, {
223+
gShare: {
224+
r: 'r',
225+
gamma: 'gamma',
226+
i: 1, // USER position
227+
j: 3, // BITGO position
228+
n: 4,
229+
},
230+
});
231+
232+
(bitgo as any).getGPGKeys = getGPGKeysStub;
233+
234+
const result = await handleEddsaSigning(
235+
bitgo,
236+
wallet,
237+
txRequest.txRequestId,
238+
enclavedExpressClient,
239+
userPubKey,
240+
reqId,
241+
);
242+
243+
result.should.eql({
244+
...txRequest,
245+
state: 'signed',
246+
});
247+
248+
getTxRequestNock.done();
249+
exchangeCommitmentsNock.done();
250+
offerRShareNock.done();
251+
getBitgoRShareNock.done();
252+
sendGShareNock.done();
253+
finalGetTxRequestNock.done();
254+
signMpcCommitmentNockEbe.done();
255+
signMpcRShareNockEbe.done();
256+
signMpcGShareNockEbe.done();
257+
});
258+
});

0 commit comments

Comments
 (0)