Skip to content

Commit c1cfe09

Browse files
authored
Merge pull request #65 from BitGo/WP-5166-eddsa-recovery-v2
feat: sign eddsa recovery transaction
2 parents acb3608 + 88eac6b commit c1cfe09

File tree

16 files changed

+1548
-844
lines changed

16 files changed

+1548
-844
lines changed

masterBitgoExpress.json

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -759,17 +759,42 @@
759759
"schema": {
760760
"type": "object",
761761
"properties": {
762-
"userPub": {
763-
"type": "string"
764-
},
765-
"backupPub": {
766-
"type": "string"
762+
"isTssRecovery": {
763+
"type": "boolean"
767764
},
768-
"bitgoPub": {
769-
"type": "string"
765+
"tssRecoveryParams": {
766+
"type": "object",
767+
"properties": {
768+
"commonKeychain": {
769+
"type": "string"
770+
}
771+
},
772+
"required": [
773+
"commonKeychain"
774+
]
770775
},
771-
"walletContractAddress": {
772-
"type": "string"
776+
"multiSigRecoveryParams": {
777+
"type": "object",
778+
"properties": {
779+
"backupPub": {
780+
"type": "string"
781+
},
782+
"bitgoPub": {
783+
"type": "string"
784+
},
785+
"userPub": {
786+
"type": "string"
787+
},
788+
"walletContractAddress": {
789+
"type": "string"
790+
}
791+
},
792+
"required": [
793+
"backupPub",
794+
"bitgoPub",
795+
"userPub",
796+
"walletContractAddress"
797+
]
773798
},
774799
"recoveryDestinationAddress": {
775800
"type": "string"
@@ -803,11 +828,7 @@
803828
}
804829
},
805830
"required": [
806-
"userPub",
807-
"backupPub",
808-
"walletContractAddress",
809-
"recoveryDestinationAddress",
810-
"apiKey"
831+
"recoveryDestinationAddress"
811832
]
812833
}
813834
}

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@
2727
"@api-ts/typed-express-router": "^1.1.13",
2828
"@bitgo/sdk-core": "^35.3.0",
2929
"@bitgo-beta/sdk-lib-mpc": "8.2.1-alpha.291",
30+
"@bitgo/sdk-coin-ada": "^4.11.5",
31+
"@bitgo/sdk-coin-dot": "^4.3.5",
32+
"@bitgo/sdk-coin-sui": "^5.15.5",
33+
"@bitgo/sdk-coin-near": "^2.7.0",
34+
"@bitgo/sdk-coin-sol": "^4.12.5",
3035
"bitgo": "^48.1.0",
3136
"@bitgo/abstract-utxo": "^9.21.4",
3237
"@bitgo/statics": "^54.6.0",
@@ -44,6 +49,9 @@
4449
"winston": "^3.11.0",
4550
"zod": "^3.25.48"
4651
},
52+
"resolutions": {
53+
"@bitgo/sdk-core": "^35.3.0"
54+
},
4755
"devDependencies": {
4856
"@api-ts/openapi-generator": "^5.7.0",
4957
"@types/body-parser": "^1.17.0",
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import 'should';
2+
import nock from 'nock';
3+
import sinon from 'sinon';
4+
import supertest from 'supertest';
5+
import { Utils } from '@bitgo/sdk-coin-sol';
6+
import * as kmsUtils from '../../../api/enclaved/utils';
7+
import { app as expressApp } from '../../../enclavedApp';
8+
import { AppMode, EnclavedConfig, TlsMode } from '../../../shared/types';
9+
10+
describe('EdDSA Recovery Signing', () => {
11+
let agent: supertest.SuperTest<supertest.Test>;
12+
const config: EnclavedConfig = {
13+
appMode: AppMode.ENCLAVED,
14+
port: 0,
15+
bind: 'localhost',
16+
timeout: 60000,
17+
logFile: '',
18+
tlsMode: TlsMode.DISABLED,
19+
mtlsRequestCert: false,
20+
allowSelfSigned: true,
21+
kmsUrl: 'kms.example.com',
22+
};
23+
24+
const commonKeychain =
25+
'e6af376e5e8cb910688746ee78ad6bb5072818aaa70cf345e172d1728d3740fd0018a89bd38b25e63c1c669862b565fc151ba135a11fb95a6bdf948c2822a1ed';
26+
const signableHex =
27+
'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAED1l2GN0NLHRG0qHWRP6mckcSGuMt1oLCXmHg+B4OFFqpDCXlFFOMMuhONo9GflGJ/CFuIQFPG0ToHYr8qudDrvQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuQDjxexg8jPVFGuhkiej/ogeUNLSvaV6hS1u9xWv2YQBAgIAAQwCAAAA+HAeAAAAAAA=';
28+
const derivationPath = 'm/0';
29+
30+
const userPrvShare = {
31+
uShare: {
32+
i: 1,
33+
t: 2,
34+
n: 3,
35+
y: 'e7d6491f125400cd25cdea85b33f131e0fbdb5681f2e32a7dc57ec9ba7efc7d2',
36+
seed: 'e7247dccc0003331932a882eb93e592cb818e55a55d661cbac2f33b7b0e9e50c',
37+
chaincode: '9b7190d99201cb97ccb0a9b85248dd5253f379c854a7c3b1515c6f99e8c574d8',
38+
},
39+
bitgoYShare: {
40+
i: 1,
41+
j: 3,
42+
y: '1972f140e84e0c55ed8de80c72fa3baa9e36add9c90b038ce0e88c5c641e1e72',
43+
v: 'c38f3af39f3f6a4a248b20dd8180a1488ebfbaf44a7449760ceda95df1ba21b4',
44+
u: '67ea6988da9e3f84de92be2f8acc7b2352c86699cb9ae15d7ce316237f138a0d',
45+
chaincode: '82500a3f5d6ff41b764bb892c832c1c88b07fa1943e45afb8a05eb7aae66b869',
46+
},
47+
backupYShare: {
48+
i: 1,
49+
j: 2,
50+
y: '526502a209e56ec7f935ff7e4186b42b691e16fdda66c73cd796a6e10c8e5b10',
51+
v: '9a2baa9f8141c12f24dea60c5e3ac002536563eafecc9bbb94d39edb18b37297',
52+
u: 'fb995729e12302ecb9e740ccc6cb0001449125abc7210f6eaf12666823e50305',
53+
chaincode: 'e2570d82e4196632f920044d4839c6e136202d5408939aad907d397790f674ac',
54+
},
55+
};
56+
57+
const backupPrvShare = {
58+
uShare: {
59+
i: 2,
60+
t: 2,
61+
n: 3,
62+
y: '526502a209e56ec7f935ff7e4186b42b691e16fdda66c73cd796a6e10c8e5b10',
63+
seed: '931322cb88f58ac02a4a654e73077046c608602329fddb73a3ac684cd661a480',
64+
chaincode: 'e2570d82e4196632f920044d4839c6e136202d5408939aad907d397790f674ac',
65+
},
66+
bitgoYShare: {
67+
i: 2,
68+
j: 3,
69+
y: '1972f140e84e0c55ed8de80c72fa3baa9e36add9c90b038ce0e88c5c641e1e72',
70+
v: 'c38f3af39f3f6a4a248b20dd8180a1488ebfbaf44a7449760ceda95df1ba21b4',
71+
u: '97d4a560c35e5f5b7d511e689a305fce4a344ebc24632344fa2a9aff75803605',
72+
chaincode: '82500a3f5d6ff41b764bb892c832c1c88b07fa1943e45afb8a05eb7aae66b869',
73+
},
74+
userYShare: {
75+
i: 2,
76+
j: 1,
77+
y: 'e7d6491f125400cd25cdea85b33f131e0fbdb5681f2e32a7dc57ec9ba7efc7d2',
78+
v: '4174c7d5fea5922d1f70fb5e86049027cc1ee14fd2b276f8ac2bcc5887d89102',
79+
u: 'bd082934f69a8442187e782920c16d09b83cab6ecfa6486a06fd01ea5be75307',
80+
chaincode: '9b7190d99201cb97ccb0a9b85248dd5253f379c854a7c3b1515c6f99e8c574d8',
81+
},
82+
};
83+
84+
beforeEach(() => {
85+
nock.disableNetConnect();
86+
nock.enableNetConnect('127.0.0.1');
87+
agent = supertest(expressApp(config));
88+
});
89+
90+
afterEach(() => {
91+
nock.cleanAll();
92+
sinon.restore();
93+
});
94+
95+
it('should successfully sign a Solana recovery transaction', async () => {
96+
// Mock KMS key retrieval
97+
const mockRetrieveKmsPrvKey = sinon.stub(kmsUtils, 'retrieveKmsPrvKey');
98+
mockRetrieveKmsPrvKey
99+
.withArgs({
100+
pub: commonKeychain,
101+
source: 'user',
102+
cfg: config,
103+
options: { useLocalEncipherment: false },
104+
})
105+
.resolves(JSON.stringify(userPrvShare));
106+
107+
mockRetrieveKmsPrvKey
108+
.withArgs({
109+
pub: commonKeychain,
110+
source: 'backup',
111+
cfg: config,
112+
options: { useLocalEncipherment: false },
113+
})
114+
.resolves(JSON.stringify(backupPrvShare));
115+
116+
const response = await agent.post('/api/tsol/mpc/recovery').send({
117+
commonKeychain,
118+
unsignedSweepPrebuildTx: {
119+
txRequests: [
120+
{
121+
signableHex,
122+
derivationPath,
123+
unsignedTx: signableHex,
124+
},
125+
],
126+
},
127+
});
128+
129+
response.status.should.equal(200);
130+
response.body.should.have.property('txHex');
131+
Utils.validateRawTransaction(response.body.txHex, true, true);
132+
133+
// Verify KMS key retrieval calls
134+
mockRetrieveKmsPrvKey
135+
.calledWith({
136+
pub: commonKeychain,
137+
source: 'user',
138+
cfg: config,
139+
options: { useLocalEncipherment: false },
140+
})
141+
.should.be.true();
142+
143+
mockRetrieveKmsPrvKey
144+
.calledWith({
145+
pub: commonKeychain,
146+
source: 'backup',
147+
cfg: config,
148+
options: { useLocalEncipherment: false },
149+
})
150+
.should.be.true();
151+
});
152+
153+
it('should fail if user private key is missing', async () => {
154+
const mockRetrieveKmsPrvKey = sinon.stub(kmsUtils, 'retrieveKmsPrvKey');
155+
mockRetrieveKmsPrvKey
156+
.withArgs({
157+
pub: commonKeychain,
158+
source: 'user',
159+
cfg: config,
160+
options: { useLocalEncipherment: false },
161+
})
162+
.resolves(undefined);
163+
164+
const response = await agent.post('/api/tsol/mpc/recovery').send({
165+
commonKeychain,
166+
unsignedSweepPrebuildTx: {
167+
txRequests: [
168+
{
169+
signableHex,
170+
derivationPath,
171+
unsignedTx: signableHex,
172+
},
173+
],
174+
},
175+
});
176+
177+
response.status.should.equal(500);
178+
response.body.should.have.property('details', 'Missing required private keys for recovery');
179+
});
180+
181+
it('should fail if backup private key is missing', async () => {
182+
const mockRetrieveKmsPrvKey = sinon.stub(kmsUtils, 'retrieveKmsPrvKey');
183+
mockRetrieveKmsPrvKey
184+
.withArgs({
185+
pub: commonKeychain,
186+
source: 'user',
187+
cfg: config,
188+
options: { useLocalEncipherment: false },
189+
})
190+
.resolves(JSON.stringify(userPrvShare));
191+
192+
mockRetrieveKmsPrvKey
193+
.withArgs({
194+
pub: commonKeychain,
195+
source: 'backup',
196+
cfg: config,
197+
options: { useLocalEncipherment: false },
198+
})
199+
.resolves(undefined);
200+
201+
const response = await agent.post('/api/tsol/mpc/recovery').send({
202+
commonKeychain,
203+
unsignedSweepPrebuildTx: {
204+
txRequests: [
205+
{
206+
signableHex,
207+
derivationPath,
208+
unsignedTx: signableHex,
209+
},
210+
],
211+
},
212+
});
213+
214+
response.status.should.equal(500);
215+
response.body.should.have.property('details', 'Missing required private keys for recovery');
216+
});
217+
});

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ describe('POST /api/:coin/wallet/generate', () => {
144144
.get('/api/v1/client/constants')
145145
// Not sure why the nock is not matching any headers, but this works
146146
.matchHeader('accept-encoding', 'gzip, deflate')
147-
.matchHeader('bitgo-sdk-version', '48.2.1')
147+
.matchHeader('bitgo-sdk-version', '48.3.0')
148148
.reply(200, {
149149
constants: {
150150
mpc: {
@@ -493,7 +493,7 @@ describe('POST /api/:coin/wallet/generate', () => {
493493
const constantsNock = nock(bitgoApiUrl)
494494
.get('/api/v1/client/constants')
495495
.matchHeader('accept-encoding', 'gzip, deflate')
496-
.matchHeader('bitgo-sdk-version', '48.2.1')
496+
.matchHeader('bitgo-sdk-version', '48.3.0')
497497
.reply(200, {
498498
constants: {
499499
mpc: {

0 commit comments

Comments
 (0)