Skip to content

Commit 265d96d

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

23 files changed

+4114
-2860
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: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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+
logFile: '',
60+
kmsUrl: kmsUrl,
61+
tlsMode: TlsMode.DISABLED,
62+
mtlsRequestCert: false,
63+
allowSelfSigned: true,
64+
};
65+
66+
configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
67+
68+
// app setup
69+
app = enclavedApp(cfg);
70+
agent = request.agent(app);
71+
});
72+
73+
afterEach(() => {
74+
nock.cleanAll();
75+
});
76+
77+
after(() => {
78+
configStub.restore();
79+
});
80+
81+
// happy path test
82+
it('should be sign a Mpc V2 Recovery', async () => {
83+
const input = {
84+
txHex:
85+
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
86+
pub: commonKeychain,
87+
};
88+
// nocks for KMS responses
89+
nock(kmsUrl)
90+
.get(`/key/${input.pub}`)
91+
.query({ source: 'user', useLocalEncipherment: false })
92+
.reply(200, mockKmsUserResponse);
93+
nock(kmsUrl)
94+
.get(`/key/${input.pub}`)
95+
.query({ source: 'backup', useLocalEncipherment: false })
96+
.reply(200, mockKmsBackupResponse);
97+
98+
const ethLikeSignatureResponse = await agent
99+
.post(`/api/${ethLikeCoin}/mpcv2/recovery`)
100+
.set('Authorization', `Bearer ${accessToken}`)
101+
.send(input);
102+
103+
ethLikeSignatureResponse.status.should.equal(200);
104+
ethLikeSignatureResponse.body.should.have.property('txHex');
105+
ethLikeSignatureResponse.body.txHex.should.equal(input.txHex);
106+
107+
ethLikeSignatureResponse.body.should.have.property('stringifiedSignature');
108+
const ethLikeSignature = JSON.parse(ethLikeSignatureResponse.body.stringifiedSignature);
109+
ethLikeSignature.should.have.property('recid');
110+
ethLikeSignature.should.have.property('r');
111+
ethLikeSignature.should.have.property('s');
112+
ethLikeSignature.should.have.property('y');
113+
114+
const cosmosLikeSignatureResponse = await agent
115+
.post(`/api/${cosmosLikeCoin}/mpcv2/recovery`)
116+
.set('Authorization', `Bearer ${accessToken}`)
117+
.send(input);
118+
119+
cosmosLikeSignatureResponse.status.should.equal(200);
120+
cosmosLikeSignatureResponse.body.should.have.property('txHex');
121+
cosmosLikeSignatureResponse.body.txHex.should.equal(input.txHex);
122+
123+
cosmosLikeSignatureResponse.body.should.have.property('stringifiedSignature');
124+
const cosmosLikeSignature = JSON.parse(cosmosLikeSignatureResponse.body.stringifiedSignature);
125+
cosmosLikeSignature.should.have.property('recid');
126+
cosmosLikeSignature.should.have.property('r');
127+
cosmosLikeSignature.should.have.property('s');
128+
cosmosLikeSignature.should.have.property('y');
129+
});
130+
131+
// failure test cases
132+
it('should throw 400 Bad Request if coin is not ECDSA', async () => {
133+
const input = {
134+
txHex:
135+
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
136+
pub: commonKeychain,
137+
};
138+
139+
// non-ecdsa coin used in mpc v2 recovery
140+
const signatureResponse = await agent
141+
.post(`/api/${nonEcdsaCoin}/mpcv2/recovery`)
142+
.set('Authorization', `Bearer ${accessToken}`)
143+
.send(input);
144+
145+
signatureResponse.status.should.equal(400);
146+
signatureResponse.body.should.have.property('error');
147+
signatureResponse.body.error.should.equal(`Enclave does not support Mpc V2 recovery for coin family: ${nonEcdsaCoin}`);
148+
})
149+
150+
it('should throw 501 Not Implemented if coin is not implemented', async () => {
151+
const input = {
152+
txHex:
153+
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
154+
pub: commonKeychain,
155+
};
156+
157+
// nocks for KMS responses
158+
nock(kmsUrl)
159+
.get(`/key/${input.pub}`)
160+
.query({ source: 'user', useLocalEncipherment: false })
161+
.reply(200, mockKmsUserResponse);
162+
nock(kmsUrl)
163+
.get(`/key/${input.pub}`)
164+
.query({ source: 'backup', useLocalEncipherment: false })
165+
.reply(200, mockKmsBackupResponse);
166+
167+
// not implemented coin used in mpc v2 recovery
168+
const signatureResponse = await agent
169+
.post(`/api/${notImplementedCoin}/mpcv2/recovery`)
170+
.set('Authorization', `Bearer ${accessToken}`)
171+
.send(input);
172+
173+
signatureResponse.status.should.equal(501);
174+
signatureResponse.body.should.have.property('error');
175+
signatureResponse.body.error.should.equal(`Enclave does not support Mpc V2 recovery for coin family: ${notImplementedCoin}`);
176+
});
177+
178+
it('should throw 400 Bad Request if failed to construct eth transaction from message hex', async () => {
179+
const input = {
180+
txHex: 'invalid-hex',
181+
pub: commonKeychain,
182+
};
183+
184+
// nocks for KMS responses
185+
nock(kmsUrl)
186+
.get(`/key/${input.pub}`)
187+
.query({ source: 'user', useLocalEncipherment: false })
188+
.reply(200, mockKmsUserResponse);
189+
nock(kmsUrl)
190+
.get(`/key/${input.pub}`)
191+
.query({ source: 'backup', useLocalEncipherment: false })
192+
.reply(200, mockKmsBackupResponse);
193+
194+
const signatureResponse = await agent
195+
.post(`/api/${ethLikeCoin}/mpcv2/recovery`)
196+
.set('Authorization', `Bearer ${accessToken}`)
197+
.send(input);
198+
199+
signatureResponse.status.should.equal(400);
200+
signatureResponse.body.should.have.property('error');
201+
signatureResponse.body.error.should.startWith('Failed to construct eth transaction from message hex');
202+
});
203+
});

0 commit comments

Comments
 (0)