Skip to content

Commit d5bbc7e

Browse files
authored
Merge pull request #46 from BitGo/WP-4738_testing
musig recovery tests for heth on-prem
2 parents 0d70111 + a47724d commit d5bbc7e

File tree

4 files changed

+394
-0
lines changed

4 files changed

+394
-0
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import 'should';
2+
3+
import express from 'express';
4+
import nock from 'nock';
5+
import * as request from 'supertest';
6+
import { app as enclavedApp } from '../../../enclavedApp';
7+
import { AppMode, EnclavedConfig, TlsMode } from '../../../shared/types';
8+
9+
import * as sinon from 'sinon';
10+
import * as configModule from '../../../initConfig';
11+
12+
import { ebeData } from '../../mocks/ethRecoveryMusigMockData';
13+
import unsignedSweepRecJSON from '../../mocks/unsigned-sweep-prebuild-hteth-musig-recovery.json';
14+
15+
describe('recoveryMultisigTransaction', () => {
16+
let cfg: EnclavedConfig;
17+
let app: express.Application;
18+
let agent: request.SuperAgentTest;
19+
20+
// test cofig
21+
const kmsUrl = 'http://kms.invalid';
22+
const coin = 'hteth';
23+
const accessToken = 'test-token';
24+
25+
// sinon stubs
26+
let configStub: sinon.SinonStub;
27+
28+
before(() => {
29+
// nock config
30+
nock.disableNetConnect();
31+
nock.enableNetConnect('127.0.0.1');
32+
33+
// app config
34+
cfg = {
35+
appMode: AppMode.ENCLAVED,
36+
port: 0, // Let OS assign a free port
37+
bind: 'localhost',
38+
timeout: 60000,
39+
logFile: '',
40+
kmsUrl: kmsUrl,
41+
tlsMode: TlsMode.DISABLED,
42+
mtlsRequestCert: false,
43+
allowSelfSigned: true,
44+
};
45+
46+
configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
47+
48+
// app setup
49+
app = enclavedApp(cfg);
50+
agent = request.agent(app);
51+
});
52+
53+
afterEach(() => {
54+
nock.cleanAll();
55+
});
56+
57+
after(() => {
58+
configStub.restore();
59+
});
60+
61+
it('should generate a successful txHex from unsigned sweep prebuild data', async () => {
62+
const { userPub, backupPub, walletContractAddress, userPrv, backupPrv, txHexResult } = ebeData;
63+
const unsignedSweepPrebuildTx = unsignedSweepRecJSON as unknown as any;
64+
65+
const mockKmsUserResponse = {
66+
prv: userPrv,
67+
pub: userPub,
68+
source: 'user',
69+
type: 'independent',
70+
};
71+
72+
const mockKmsBackupResponse = {
73+
prv: backupPrv,
74+
pub: backupPub,
75+
source: 'backup',
76+
type: 'independent',
77+
};
78+
79+
const kmsNockUser = nock(kmsUrl)
80+
.get(`/key/${userPub}`)
81+
.query({ source: 'user' })
82+
.reply(200, mockKmsUserResponse);
83+
84+
const kmsNockBackup = nock(kmsUrl)
85+
.get(`/key/${backupPub}`)
86+
.query({ source: 'backup' })
87+
.reply(200, mockKmsBackupResponse);
88+
89+
console.warn(nock.activeMocks());
90+
console.warn(nock.isActive());
91+
92+
const response = await agent
93+
.post(`/api/${coin}/multisig/recovery`)
94+
.set('Authorization', `Bearer ${accessToken}`)
95+
.send({
96+
userPub,
97+
backupPub,
98+
apiKey: 'etherscan-api-token',
99+
unsignedSweepPrebuildTx,
100+
walletContractAddress,
101+
coinSpecificParams: undefined,
102+
});
103+
104+
response.status.should.equal(200);
105+
response.body.should.have.property('txHex', txHexResult);
106+
107+
kmsNockUser.done();
108+
kmsNockBackup.done();
109+
});
110+
111+
it('should fail when prv keys non related to pub keys', async () => {
112+
const { userPub, backupPub, walletContractAddress } = ebeData;
113+
const unsignedSweepPrebuildTx = unsignedSweepRecJSON as unknown as any;
114+
115+
// Use invalid private keys
116+
const invalidUserPrv = 'invalid-prv';
117+
const invalidBackupPrv = 'invalid-prv';
118+
119+
const mockKmsUserResponse = {
120+
prv: invalidUserPrv,
121+
pub: userPub,
122+
source: 'user',
123+
type: 'independent',
124+
};
125+
126+
const mockKmsBackupResponse = {
127+
prv: invalidBackupPrv,
128+
pub: backupPub,
129+
source: 'backup',
130+
type: 'independent',
131+
};
132+
133+
const kmsNockUser = nock(kmsUrl)
134+
.get(`/key/${userPub}`)
135+
.query({ source: 'user' })
136+
.reply(200, mockKmsUserResponse);
137+
138+
const kmsNockBackup = nock(kmsUrl)
139+
.get(`/key/${backupPub}`)
140+
.query({ source: 'backup' })
141+
.reply(200, mockKmsBackupResponse);
142+
143+
const response = await agent
144+
.post(`/api/${coin}/multisig/recovery`)
145+
.set('Authorization', `Bearer ${accessToken}`)
146+
.send({
147+
userPub,
148+
backupPub,
149+
apiKey: 'etherscan-api-token',
150+
unsignedSweepPrebuildTx,
151+
walletContractAddress,
152+
coinSpecificParams: undefined,
153+
});
154+
155+
response.status.should.equal(500);
156+
response.body.should.have.property('error');
157+
158+
kmsNockUser.done();
159+
kmsNockBackup.done();
160+
});
161+
});
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import 'should';
2+
import sinon from 'sinon';
3+
4+
import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth';
5+
import nock from 'nock';
6+
import * as request from 'supertest';
7+
import { app as expressApp } from '../../../masterExpressApp';
8+
import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
9+
import { data as ethRecoveryData } from '../../mocks/ethRecoveryMusigMockData';
10+
11+
describe('POST /api/:coin/wallet/recovery', () => {
12+
let agent: request.SuperAgentTest;
13+
const enclavedExpressUrl = 'http://enclaved.invalid';
14+
const coin = 'hteth';
15+
const accessToken = 'test-token';
16+
17+
before(() => {
18+
nock.disableNetConnect();
19+
nock.enableNetConnect('127.0.0.1');
20+
21+
const config: MasterExpressConfig = {
22+
appMode: AppMode.MASTER_EXPRESS,
23+
port: 0, // Let OS assign a free port
24+
bind: 'localhost',
25+
timeout: 60000,
26+
logFile: '',
27+
env: 'test',
28+
disableEnvCheck: true,
29+
authVersion: 2,
30+
enclavedExpressUrl: enclavedExpressUrl,
31+
enclavedExpressCert: 'dummy-cert',
32+
tlsMode: TlsMode.DISABLED,
33+
mtlsRequestCert: false,
34+
allowSelfSigned: true,
35+
};
36+
37+
const app = expressApp(config);
38+
agent = request.agent(app);
39+
});
40+
41+
afterEach(() => {
42+
nock.cleanAll();
43+
sinon.restore();
44+
});
45+
46+
it('should get the tx hex for broadcasting from eve on musig recovery ', async () => {
47+
// sdk call mock on mbe
48+
const recoverStub = sinon
49+
.stub(AbstractEthLikeNewCoins.prototype, 'recover')
50+
.resolves(ethRecoveryData.unsignedSweepPrebuildTx);
51+
52+
// the call to eve.recoverWallet(...)
53+
// that contains the calls to sdk.signTransaction
54+
const eveRecoverWalletNock = nock(enclavedExpressUrl)
55+
.post(`/api/${coin}/multisig/recovery`, {
56+
userPub: ethRecoveryData.userKey,
57+
backupPub: ethRecoveryData.backupKey,
58+
apiKey: 'etherscan-api-token',
59+
unsignedSweepPrebuildTx: ethRecoveryData.unsignedSweepPrebuildTx,
60+
coinSpecificParams: undefined,
61+
walletContractAddress: ethRecoveryData.walletContractAddress,
62+
})
63+
.reply(200, {
64+
txHex: ethRecoveryData.txHexFullSigned,
65+
});
66+
67+
// the call to our own master api express endpoint
68+
const response = await agent
69+
.post(`/api/${coin}/wallet/recovery`, (body) => {
70+
console.log('Nock received body:', body);
71+
return true;
72+
})
73+
.set('Authorization', `Bearer ${accessToken}`)
74+
.send({
75+
userPub: ethRecoveryData.userKey,
76+
backupPub: ethRecoveryData.backupKey,
77+
apiKey: 'etherscan-api-token',
78+
walletContractAddress: ethRecoveryData.walletContractAddress,
79+
recoveryDestinationAddress: ethRecoveryData.recoveryDestinationAddress,
80+
});
81+
82+
response.status.should.equal(200);
83+
response.body.should.have.property('txHex', ethRecoveryData.txHexFullSigned);
84+
sinon.assert.calledOnce(recoverStub);
85+
eveRecoverWalletNock.done();
86+
});
87+
88+
it('should fail when walletContractAddress (origin) not provided', async () => {
89+
const response = await agent
90+
.post(`/api/${coin}/wallet/recovery`)
91+
.set('Authorization', `Bearer ${accessToken}`)
92+
.send({
93+
userPub: ethRecoveryData.userKey,
94+
backupPub: ethRecoveryData.backupKey,
95+
apiKey: 'etherscan-api-token',
96+
walletContractAddress: undefined,
97+
recoveryDestinationAddress: ethRecoveryData.recoveryDestinationAddress,
98+
});
99+
100+
response.status.should.equal(400);
101+
response.body.should.have.property('error');
102+
response.body.error.should.match(/walletContractAddress/i);
103+
});
104+
it('should fail when recoveryDestinationAddress (destiny) not provided', async () => {
105+
const response = await agent
106+
.post(`/api/${coin}/wallet/recovery`)
107+
.set('Authorization', `Bearer ${accessToken}`)
108+
.send({
109+
userPub: ethRecoveryData.userKey,
110+
backupPub: ethRecoveryData.backupKey,
111+
apiKey: 'etherscan-api-token',
112+
walletContractAddress: ethRecoveryData.walletContractAddress,
113+
recoveryDestinationAddress: undefined,
114+
});
115+
116+
response.status.should.equal(400);
117+
response.body.should.have.property('error');
118+
response.body.error.should.match(/recoveryDestinationAddress/i);
119+
});
120+
it('should fail when userPub or backupPub not provided', async () => {
121+
const responseNoUserKey = await agent
122+
.post(`/api/${coin}/wallet/recovery`)
123+
.set('Authorization', `Bearer ${accessToken}`)
124+
.send({
125+
backupPub: ethRecoveryData.backupKey,
126+
apiKey: 'etherscan-api-token',
127+
walletContractAddress: ethRecoveryData.walletContractAddress,
128+
recoveryDestinationAddress: undefined,
129+
});
130+
131+
const responseNoBackupKey = await agent
132+
.post(`/api/${coin}/wallet/recovery`)
133+
.set('Authorization', `Bearer ${accessToken}`)
134+
.send({
135+
userPub: ethRecoveryData.userKey,
136+
apiKey: 'etherscan-api-token',
137+
walletContractAddress: ethRecoveryData.walletContractAddress,
138+
recoveryDestinationAddress: undefined,
139+
});
140+
141+
responseNoUserKey.status.should.equal(400);
142+
responseNoUserKey.body.should.have.property('error');
143+
responseNoUserKey.body.error.should.match(/userPub/i);
144+
145+
responseNoBackupKey.status.should.equal(400);
146+
responseNoBackupKey.body.should.have.property('error');
147+
responseNoBackupKey.body.error.should.match(/backupPub/i);
148+
});
149+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const walletContractAddress = '0x223fe2adcc8f28d8a46f72f7f355117d2727554d';
2+
3+
export const data = {
4+
walletContractAddress,
5+
userKey:
6+
'xpub661MyMwAqRbcFigezGWEYSbCPVuaUmvnp1u7iEpH9YsKU6uYQtPANvudjgAo82QRHXsUieMqKeB1xEj89VUKU1ugtmyAZ3xzNEbHPexxgKK',
7+
backupKey:
8+
'xpub661MyMwAqRbcGbCirzmQsUJT2eidt9tFLw2m77w6FiKco6TKu49CP3GkHF88xGCpvqkP93SYMAarfyWAn8UWevQtNT6pDo8xH7xmf6GqK6e',
9+
unsignedSweepPrebuildTx: {
10+
tx: 'f9012b808504a817c8008307a12094223fe2adcc8f28d8a46f72f7f355117d2727554d80b9010439125215000000000000000000000000e7d07af8e3e7472ea8391a3372ab98d04ac4df200000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000006851a693000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808080',
11+
userKey:
12+
'xpub661MyMwAqRbcFigezGWEYSbCPVuaUmvnp1u7iEpH9YsKU6uYQtPANvudjgAo82QRHXsUieMqKeB1xEj89VUKU1ugtmyAZ3xzNEbHPexxgKK',
13+
backupKey:
14+
'xpub661MyMwAqRbcGbCirzmQsUJT2eidt9tFLw2m77w6FiKco6TKu49CP3GkHF88xGCpvqkP93SYMAarfyWAn8UWevQtNT6pDo8xH7xmf6GqK6e',
15+
coin: 'hteth',
16+
gasPrice: 20000000000,
17+
gasLimit: 500000,
18+
recipients: [
19+
{
20+
address: '0xe7d07af8e3e7472ea8391a3372ab98d04ac4df20',
21+
amount: '1000000000000000000',
22+
},
23+
],
24+
walletContractAddress,
25+
amount: '1000000000000000000',
26+
backupKeyNonce: 0,
27+
recipient: {
28+
address: '0xe7d07af8e3e7472ea8391a3372ab98d04ac4df20',
29+
amount: '1000000000000000000',
30+
},
31+
expireTime: 1750181523,
32+
contractSequenceId: '1',
33+
nextContractSequenceId: '1',
34+
},
35+
txHexFullSigned:
36+
'xpub661MyMwAqRbcGLBsaNVtc8bhB7dN8fzf3JTEKhviDUMDz11HkcHNXRSV6tk2jsqPhXnqLJPUHy8VMSjTr4hPFimRWdFk3eaLEM8VBUbZdQE',
37+
recoveryDestinationAddress: '0x927324f364a6fd1bf4648310a445b58063f5bb64',
38+
};
39+
40+
export const ebeData = {
41+
userPub:
42+
'xpub661MyMwAqRbcGvbtjkhDJ5uiFWb6eK9nFQmLLgW1jDwJzJ2vQPyp2uKLmUBgGZKiA9HDUFYfuDoyP1dF3tj3Ucod25tmiEG2k26UX97S3Wz',
43+
backupPub:
44+
'xpub661MyMwAqRbcF4KcM9ZR5gXg5M3z1gKerho5XqmQLBG1Yc72U9dvULLrcaX92RAjbkRqJvzYAmmj5Hcnts1Tdvhy9csZuiuMowuxvNHEgrn',
45+
walletContractAddress: '0x74ede0b3a0d6a3688b0c85d96e7297dae5d4a951',
46+
userPrv:
47+
'xprv9s21ZrQH143K4SXRdjACvwxyhUkcErRvtBqjYJ6QAtQL7VhmrrfZV6zrvBkFHG698ns5qicM1FS9jUwz1UqxQsungfaoMowPDw9HooBNZgw',
48+
backupPrv:
49+
'xprv9s21ZrQH143K2aF9F82QiYawXKDVcDboVUsUjTMnmqj2fomsvcKfvY2NmHXgSdqPZrG8vwPDJSqBSt8Bv3sjzc9WbGdBrmWr8vfgYHDqSFH',
50+
txHexResult:
51+
'02f901d5824268808502540be4008504a817c80083030d409474ede0b3a0d6a3688b0c85d96e7297dae5d4a95180b9016439125215000000000000000000000000927324f364a6fd1bf4648310a445b58063f5bb64000000000000000000000000000000000000000000000000053444835ec5800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000006867cba3000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004110b2f41b55a981d21efdfe04cff1426fd45d0061bda22f1ff01d589a54baf1b3034cfdf164410a28b0f768fe452cc1568dcc18601891fc08b3b854c7f0ddb4791b00000000000000000000000000000000000000000000000000000000000000c001a002f5ff1cc3b49dedf2cc0f8f3c811175eb2fb9d4aa4ea1422342c08604459d98a0368b6909b10c80da2b3b3ef04670c02d02f79fb63e287ba6e7e6cb4b32809f57',
52+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"tx": "02f90135824268808502540be4008504a817c80083030d409474ede0b3a0d6a3688b0c85d96e7297dae5d4a95180b9010439125215000000000000000000000000927324f364a6fd1bf4648310a445b58063f5bb64000000000000000000000000000000000000000000000000053444835ec5800000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000006867cba3000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0808080",
3+
"userKey": "xpub661MyMwAqRbcGvbtjkhDJ5uiFWb6eK9nFQmLLgW1jDwJzJ2vQPyp2uKLmUBgGZKiA9HDUFYfuDoyP1dF3tj3Ucod25tmiEG2k26UX97S3Wz",
4+
"backupKey": "xpub661MyMwAqRbcF4KcM9ZR5gXg5M3z1gKerho5XqmQLBG1Yc72U9dvULLrcaX92RAjbkRqJvzYAmmj5Hcnts1Tdvhy9csZuiuMowuxvNHEgrn",
5+
"coin": "hteth",
6+
"gasPrice": "20000000000",
7+
"gasLimit": "200000",
8+
"recipients": [
9+
{
10+
"address": "0x927324f364a6fd1bf4648310a445b58063f5bb64",
11+
"amount": "375000000000000000"
12+
}
13+
],
14+
"walletContractAddress": "0x74ede0b3a0d6a3688b0c85d96e7297dae5d4a951",
15+
"amount": "375000000000000000",
16+
"backupKeyNonce": 0,
17+
"eip1559": {
18+
"maxFeePerGas": 20000000000,
19+
"maxPriorityFeePerGas": 10000000000
20+
},
21+
"replayProtectionOptions": {
22+
"chain": 17000,
23+
"hardfork": "london"
24+
},
25+
"recipient": {
26+
"address": "0x927324f364a6fd1bf4648310a445b58063f5bb64",
27+
"amount": "375000000000000000"
28+
},
29+
"expireTime": 1751632803,
30+
"contractSequenceId": 1,
31+
"nextContractSequenceId": 1
32+
}

0 commit comments

Comments
 (0)