Skip to content

Commit fb598d9

Browse files
committed
feat(ebe, mbe): add mpcv2 recovery support for eth-like coins
Ticket: WP-5168
1 parent efbf60e commit fb598d9

23 files changed

+4138
-2859
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"@bitgo-beta/statics": "15.1.1-beta.1033",
102102
"@bitgo/wasm-miniscript": "2.0.0-beta.7",
103103
"@commitlint/config-conventional": "^19.8.1",
104+
"@ethereumjs/tx": "^3.3.0",
104105
"body-parser": "^1.20.3",
105106
"connect-timeout": "^1.9.0",
106107
"debug": "^3.1.0",
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { AppMode, EnclavedConfig, TlsMode } from '../../../initConfig';
2+
import { app as enclavedApp } from '../../../enclavedApp';
3+
4+
import express from 'express';
5+
import nock from 'nock';
6+
import 'should';
7+
import * as request from 'supertest';
8+
import * as sinon from 'sinon';
9+
import * as configModule from '../../../initConfig';
10+
import { DklsTypes, DklsUtils } from '@bitgo-beta/sdk-lib-mpc';
11+
12+
describe('recoveryMpcV2', async () => {
13+
let cfg: EnclavedConfig;
14+
let app: express.Application;
15+
let agent: request.SuperAgentTest;
16+
17+
// test config
18+
const kmsUrl = 'http://kms.invalid';
19+
const ethLikeCoin = 'hteth';
20+
const cosmosLikeCoin = 'tsei';
21+
const nonEcdsaCoin = 'btc';
22+
const notImplementedCoin = '';
23+
const accessToken = 'test-token';
24+
25+
// sinon stubs
26+
let configStub: sinon.SinonStub;
27+
28+
// kms nocks setup
29+
const [userShare, backupShare] = await DklsUtils.generateDKGKeyShares();
30+
const userKeyShare = userShare.getKeyShare().toString('base64');
31+
const backupKeyShare = backupShare.getKeyShare().toString('base64');
32+
const commonKeychain = DklsTypes.getCommonKeychain(userShare.getKeyShare());
33+
34+
const mockKmsUserResponse = {
35+
prv: JSON.stringify(userKeyShare),
36+
pub: commonKeychain,
37+
source: 'user',
38+
type: 'tss',
39+
};
40+
41+
const mockKmsBackupResponse = {
42+
prv: JSON.stringify(backupKeyShare),
43+
pub: commonKeychain,
44+
source: 'backup',
45+
type: 'tss',
46+
};
47+
48+
before(async () => {
49+
// nock config
50+
nock.disableNetConnect();
51+
nock.enableNetConnect('127.0.0.1');
52+
53+
// app config
54+
cfg = {
55+
appMode: AppMode.ENCLAVED,
56+
port: 0, // Let OS assign a free port
57+
bind: 'localhost',
58+
timeout: 60000,
59+
kmsUrl: kmsUrl,
60+
httpLoggerFile: '',
61+
tlsMode: TlsMode.DISABLED,
62+
allowSelfSigned: true,
63+
};
64+
65+
configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
66+
67+
// app setup
68+
app = enclavedApp(cfg);
69+
agent = request.agent(app);
70+
});
71+
72+
afterEach(() => {
73+
nock.cleanAll();
74+
});
75+
76+
after(() => {
77+
configStub.restore();
78+
});
79+
80+
// happy path test
81+
it('should be sign a Mpc V2 Recovery', async () => {
82+
const input = {
83+
txHex:
84+
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
85+
pub: commonKeychain,
86+
};
87+
// nocks for KMS responses
88+
nock(kmsUrl)
89+
.get(`/key/${input.pub}`)
90+
.query({ source: 'user', useLocalEncipherment: false })
91+
.reply(200, mockKmsUserResponse);
92+
nock(kmsUrl)
93+
.get(`/key/${input.pub}`)
94+
.query({ source: 'backup', useLocalEncipherment: false })
95+
.reply(200, mockKmsBackupResponse);
96+
97+
const ethLikeSignatureResponse = await agent
98+
.post(`/api/${ethLikeCoin}/mpcv2/recovery`)
99+
.set('Authorization', `Bearer ${accessToken}`)
100+
.send(input);
101+
102+
ethLikeSignatureResponse.status.should.equal(200);
103+
ethLikeSignatureResponse.body.should.have.property('txHex');
104+
ethLikeSignatureResponse.body.txHex.should.equal(input.txHex);
105+
106+
ethLikeSignatureResponse.body.should.have.property('stringifiedSignature');
107+
const ethLikeSignature = JSON.parse(ethLikeSignatureResponse.body.stringifiedSignature);
108+
ethLikeSignature.should.have.property('recid');
109+
ethLikeSignature.should.have.property('r');
110+
ethLikeSignature.should.have.property('s');
111+
ethLikeSignature.should.have.property('y');
112+
113+
const cosmosLikeSignatureResponse = await agent
114+
.post(`/api/${cosmosLikeCoin}/mpcv2/recovery`)
115+
.set('Authorization', `Bearer ${accessToken}`)
116+
.send(input);
117+
118+
cosmosLikeSignatureResponse.status.should.equal(200);
119+
cosmosLikeSignatureResponse.body.should.have.property('txHex');
120+
cosmosLikeSignatureResponse.body.txHex.should.equal(input.txHex);
121+
122+
cosmosLikeSignatureResponse.body.should.have.property('stringifiedSignature');
123+
const cosmosLikeSignature = JSON.parse(cosmosLikeSignatureResponse.body.stringifiedSignature);
124+
cosmosLikeSignature.should.have.property('recid');
125+
cosmosLikeSignature.should.have.property('r');
126+
cosmosLikeSignature.should.have.property('s');
127+
cosmosLikeSignature.should.have.property('y');
128+
});
129+
130+
// failure test cases
131+
it('should throw 400 Bad Request if coin is not ECDSA', async () => {
132+
const input = {
133+
txHex:
134+
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
135+
pub: commonKeychain,
136+
};
137+
138+
// non-ecdsa coin used in mpc v2 recovery
139+
const signatureResponse = await agent
140+
.post(`/api/${nonEcdsaCoin}/mpcv2/recovery`)
141+
.set('Authorization', `Bearer ${accessToken}`)
142+
.send(input);
143+
144+
signatureResponse.status.should.equal(400);
145+
signatureResponse.body.should.have.property('error');
146+
signatureResponse.body.error.should.equal(
147+
`Enclave does not support Mpc V2 recovery for coin family: ${nonEcdsaCoin}`,
148+
);
149+
});
150+
151+
it('should throw 501 Not Implemented if coin is not implemented', async () => {
152+
const input = {
153+
txHex:
154+
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
155+
pub: commonKeychain,
156+
};
157+
158+
// nocks for KMS responses
159+
nock(kmsUrl)
160+
.get(`/key/${input.pub}`)
161+
.query({ source: 'user', useLocalEncipherment: false })
162+
.reply(200, mockKmsUserResponse);
163+
nock(kmsUrl)
164+
.get(`/key/${input.pub}`)
165+
.query({ source: 'backup', useLocalEncipherment: false })
166+
.reply(200, mockKmsBackupResponse);
167+
168+
// not implemented coin used in mpc v2 recovery
169+
const signatureResponse = await agent
170+
.post(`/api/${notImplementedCoin}/mpcv2/recovery`)
171+
.set('Authorization', `Bearer ${accessToken}`)
172+
.send(input);
173+
174+
signatureResponse.status.should.equal(501);
175+
signatureResponse.body.should.have.property('error');
176+
signatureResponse.body.error.should.equal(
177+
`Enclave does not support Mpc V2 recovery for coin family: ${notImplementedCoin}`,
178+
);
179+
});
180+
181+
it('should throw 400 Bad Request if failed to construct eth transaction from message hex', async () => {
182+
const input = {
183+
txHex: 'invalid-hex',
184+
pub: commonKeychain,
185+
};
186+
187+
// nocks for KMS responses
188+
nock(kmsUrl)
189+
.get(`/key/${input.pub}`)
190+
.query({ source: 'user', useLocalEncipherment: false })
191+
.reply(200, mockKmsUserResponse);
192+
nock(kmsUrl)
193+
.get(`/key/${input.pub}`)
194+
.query({ source: 'backup', useLocalEncipherment: false })
195+
.reply(200, mockKmsBackupResponse);
196+
197+
const signatureResponse = await agent
198+
.post(`/api/${ethLikeCoin}/mpcv2/recovery`)
199+
.set('Authorization', `Bearer ${accessToken}`)
200+
.send(input);
201+
202+
signatureResponse.status.should.equal(400);
203+
signatureResponse.body.should.have.property('error');
204+
signatureResponse.body.error.should.startWith(
205+
'Failed to construct eth transaction from message hex',
206+
);
207+
});
208+
});

0 commit comments

Comments
 (0)