Skip to content

Commit e908fe6

Browse files
feat: eddsa recover txn signature
Ticket: WP-5166
1 parent 454b4c6 commit e908fe6

File tree

6 files changed

+16
-246
lines changed

6 files changed

+16
-246
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('POST /api/:coin/wallet/recovery', () => {
7575
userPub: ethRecoveryData.userKey,
7676
backupPub: ethRecoveryData.backupKey,
7777
walletContractAddress: ethRecoveryData.walletContractAddress,
78-
bitgoPub: '',
78+
bitgoPub: undefined,
7979
},
8080
apiKey: 'etherscan-api-token',
8181
recoveryDestinationAddress: ethRecoveryData.recoveryDestinationAddress,
@@ -158,4 +158,18 @@ describe('POST /api/:coin/wallet/recovery', () => {
158158
responseNoBackupKey.body.should.have.property('error');
159159
responseNoBackupKey.body.error.should.match(/backupPub/i);
160160
});
161+
162+
it('should fail TSS recovery when common keychain is missing', async () => {
163+
const response = await agent
164+
.post(`/api/${coin}/wallet/recovery`)
165+
.set('Authorization', `Bearer ${accessToken}`)
166+
.send({
167+
isTssRecovery: true,
168+
recoveryDestinationAddress: ethRecoveryData.recoveryDestinationAddress,
169+
});
170+
171+
response.status.should.equal(400);
172+
response.body.should.have.property('error');
173+
response.body.error.should.match(/TSS recovery parameters are required/i);
174+
});
161175
});
Lines changed: 0 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,148 +0,0 @@
1-
import { BitGoAPI } from '@bitgo/sdk-api';
2-
import {
3-
BaseCoin,
4-
BaseTransactionBuilder,
5-
Eddsa,
6-
EDDSAMethods,
7-
EDDSAMethodTypes,
8-
PublicKey,
9-
} from '@bitgo/sdk-core';
10-
import { Ed25519Bip32HdTree } from '@bitgo/sdk-lib-mpc';
11-
import { CoinFamily, coins } from '@bitgo/statics';
12-
import { type KeyPair as SolKeyPair } from '@bitgo/sdk-coin-sol';
13-
import { retrieveKmsPrvKey } from '../utils';
14-
import { EnclavedConfig } from '../../../shared/types';
15-
import logger from '../../../logger';
16-
17-
async function setupTransactionBuilder(
18-
sdk: BitGoAPI,
19-
coinFamily: CoinFamily,
20-
signableHex: string,
21-
accountId: string,
22-
): Promise<{ txBuilder: BaseTransactionBuilder; publicKey: string }> {
23-
let modules;
24-
switch (coinFamily) {
25-
case CoinFamily.NEAR:
26-
modules = await import('@bitgo/sdk-coin-near');
27-
break;
28-
case CoinFamily.DOT:
29-
modules = await import('@bitgo/sdk-coin-dot');
30-
break;
31-
case CoinFamily.SUI:
32-
modules = await import('@bitgo/sdk-coin-sui');
33-
break;
34-
case CoinFamily.ADA:
35-
modules = await import('@bitgo/sdk-coin-ada');
36-
break;
37-
case CoinFamily.SOL:
38-
modules = await import('@bitgo/sdk-coin-sol');
39-
break;
40-
default:
41-
throw new Error(`Unsupported coin family: ${coinFamily}`);
42-
}
43-
44-
const { TransactionBuilderFactory, register, KeyPair } = modules;
45-
register(sdk);
46-
47-
const staticCoin = coins.get(coinFamily);
48-
try {
49-
const keyPair = new KeyPair({ pub: accountId });
50-
51-
let txBuilder;
52-
let publicKey: string;
53-
if (coinFamily === CoinFamily.SOL) {
54-
txBuilder = new TransactionBuilderFactory(staticCoin).from(signableHex);
55-
// For Solana, we need to use the getAddress method to derive the public key
56-
publicKey = (keyPair as SolKeyPair).getAddress();
57-
} else {
58-
txBuilder = new TransactionBuilderFactory(staticCoin).from(
59-
Buffer.from(signableHex, 'hex').toString('base64'),
60-
);
61-
publicKey = keyPair.getKeys().pub;
62-
}
63-
64-
return { txBuilder, publicKey };
65-
} catch (error) {
66-
logger.error(error);
67-
throw error;
68-
}
69-
}
70-
71-
export type SignEddsaRecoveryTransactionParams = {
72-
sdk: BitGoAPI;
73-
request: {
74-
commonKeychain: string;
75-
signableHex: string;
76-
derivationPath: string;
77-
};
78-
cfg: EnclavedConfig;
79-
coin: BaseCoin;
80-
};
81-
82-
export async function signEddsaRecoveryTransaction({
83-
sdk,
84-
cfg,
85-
request,
86-
coin,
87-
}: SignEddsaRecoveryTransactionParams) {
88-
let publicKey = '';
89-
logger.info(`Received request ${JSON.stringify(request)}`);
90-
91-
const hdTree = await Ed25519Bip32HdTree.initialize();
92-
const MPC = await Eddsa.initialize(hdTree);
93-
94-
const accountId = MPC.deriveUnhardened(request.commonKeychain, request.derivationPath).slice(
95-
0,
96-
64,
97-
);
98-
99-
const { txBuilder, publicKey: derivedKey } = await setupTransactionBuilder(
100-
sdk,
101-
coin.getFamily() as CoinFamily,
102-
request.signableHex,
103-
accountId,
104-
);
105-
106-
publicKey = derivedKey;
107-
// Get user and backup private keys
108-
const userPrv = await retrieveKmsPrvKey({
109-
pub: request.commonKeychain.toString(),
110-
source: 'user',
111-
cfg,
112-
options: { useLocalEncipherment: false },
113-
});
114-
115-
const backupPrv = await retrieveKmsPrvKey({
116-
pub: request.commonKeychain.toString(),
117-
source: 'backup',
118-
cfg,
119-
options: { useLocalEncipherment: false },
120-
});
121-
122-
if (!userPrv || !backupPrv) {
123-
throw new Error('Missing required private keys for recovery');
124-
}
125-
126-
const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;
127-
const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;
128-
129-
try {
130-
const signatureHex = await EDDSAMethods.getTSSSignature(
131-
userSigningMaterial,
132-
backupSigningMaterial,
133-
request.derivationPath,
134-
await txBuilder.build(),
135-
);
136-
137-
const publicKeyObj = { pub: publicKey };
138-
txBuilder.addSignature(publicKeyObj as PublicKey, signatureHex);
139-
const signedTx = await txBuilder.build();
140-
const serializedTx = signedTx.toBroadcastFormat();
141-
142-
return {
143-
txHex: serializedTx,
144-
};
145-
} catch (error) {
146-
throw error;
147-
}
148-
}

src/api/master/clients/enclavedExpressClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export class EnclavedExpressClient {
174174
userPub: string;
175175
backupPub: string;
176176
apiKey: string;
177-
coinSpecificParams?: Record<string, unknown>;
177+
coinSpecificParams: any;
178178
walletContractAddress: string;
179179
}): Promise<SignedTransaction> {
180180
if (!this.coin) {

src/api/master/handlers/recoveryWallet.ts

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ import {
1717
} from '../../../shared/recoveryUtils';
1818
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
1919
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
20-
import { recoverEddsaWallets } from './recoverEddsaWallets';
21-
import { EnvironmentName } from '../../../shared/types';
22-
import logger from '../../../logger';
2320

2421
interface RecoveryParams {
2522
userKey: string;
@@ -69,35 +66,6 @@ async function handleEthLikeRecovery(
6966
}
7067
}
7168

72-
async function handleEddsaRecovery(
73-
bitgo: BitGoAPI,
74-
sdkCoin: BaseCoin,
75-
commonRecoveryParams: RecoveryParams,
76-
enclavedExpressClient: EnclavedExpressClient,
77-
params: EnclavedRecoveryParams,
78-
) {
79-
const { recoveryDestination, userKey } = commonRecoveryParams;
80-
try {
81-
const unsignedSweepPrebuildTx = await recoverEddsaWallets(bitgo, sdkCoin, {
82-
bitgoKey: userKey,
83-
recoveryDestination,
84-
apiKey: params.apiKey,
85-
});
86-
logger.info('Unsigned sweep tx: ', JSON.stringify(unsignedSweepPrebuildTx, null, 2));
87-
88-
return await enclavedExpressClient.recoveryMPC({
89-
userPub: params.userPub,
90-
backupPub: params.backupPub,
91-
apiKey: params.apiKey,
92-
unsignedSweepPrebuildTx,
93-
coinSpecificParams: params.coinSpecificParams,
94-
walletContractAddress: params.walletContractAddress,
95-
});
96-
} catch (err) {
97-
throw err;
98-
}
99-
}
100-
10169
export type UtxoCoinSpecificRecoveryParams = Pick<
10270
Parameters<AbstractUtxoCoin['recover']>[0],
10371
| 'apiKey'

src/enclavedBitgoExpress/routers/enclavedApiSpec.ts

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,6 @@ import { DklsDkg, DklsTypes } from '@bitgo-beta/sdk-lib-mpc';
2828
import { ecdsaMPCv2Initialize } from '../../api/enclaved/handlers/ecdsaMPCv2Initialize';
2929
import { ecdsaMPCv2Round } from '../../api/enclaved/handlers/ecdsaMPCv2Round';
3030
import { ecdsaMPCv2Finalize } from '../../api/enclaved/handlers/ecdsaMPCv2Finalize';
31-
import { signEddsaRecoveryTransaction } from '../../api/enclaved/handlers/signEddsaRecoveryTransaction';
32-
import { isEddsaCoin } from '../../shared/coinUtils';
33-
import { MethodNotImplementedError } from '@bitgo/sdk-core';
3431

3532
// Request type for /key/independent endpoint
3633
const IndependentKeyRequest = {
@@ -85,31 +82,6 @@ const RecoveryMultisigResponse: HttpResponse = {
8582
}),
8683
};
8784

88-
const RecoveryMpcRequest = {
89-
commonKeychain: t.string,
90-
unsignedSweepPrebuildTx: t.type({
91-
txRequests: t.array(
92-
t.type({
93-
unsignedTx: t.string,
94-
signableHex: t.string,
95-
derivationPath: t.string,
96-
}),
97-
),
98-
}),
99-
};
100-
101-
export type RecoveryMpcRequest = typeof RecoveryMpcRequest;
102-
103-
const RecoveryMpcResponse: HttpResponse = {
104-
200: t.type({
105-
txHex: t.string,
106-
}), // the full signed tx
107-
500: t.type({
108-
error: t.string,
109-
details: t.string,
110-
}),
111-
};
112-
11385
// Request type for /mpc/sign endpoint
11486
const SignMpcRequest = {
11587
source: t.string,
@@ -326,20 +298,6 @@ export const EnclavedAPiSpec = apiSpec({
326298
description: 'Recover a multisig transaction',
327299
}),
328300
},
329-
'v1.mpc.recovery': {
330-
post: httpRoute({
331-
method: 'POST',
332-
path: `/api/{coin}/mpc/recovery`,
333-
request: httpRequest({
334-
params: {
335-
coin: t.string,
336-
},
337-
body: RecoveryMpcRequest,
338-
}),
339-
response: RecoveryMpcResponse,
340-
description: 'Sign a recovery transaction with EdDSA user & backup keyshares',
341-
}),
342-
},
343301
'v1.key.independent': {
344302
post: httpRoute({
345303
method: 'POST',
@@ -522,28 +480,6 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter<typeof
522480
}),
523481
]);
524482

525-
router.post('v1.mpc.recovery', [
526-
responseHandler<EnclavedConfig>(async (req) => {
527-
const typedReq = req as EnclavedApiSpecRouteRequest<'v1.mpc.recovery', 'post'>;
528-
const coin = typedReq.bitgo.coin(typedReq.decoded.coin);
529-
if (isEddsaCoin(coin)) {
530-
const result = await signEddsaRecoveryTransaction({
531-
sdk: typedReq.bitgo,
532-
request: {
533-
commonKeychain: typedReq.decoded.commonKeychain,
534-
signableHex: typedReq.decoded.unsignedSweepPrebuildTx.txRequests[0].signableHex,
535-
derivationPath: typedReq.decoded.unsignedSweepPrebuildTx.txRequests[0].derivationPath,
536-
},
537-
cfg: typedReq.config,
538-
coin,
539-
});
540-
return Response.ok(result);
541-
} else {
542-
throw new MethodNotImplementedError();
543-
}
544-
}),
545-
]);
546-
547483
router.post('v1.mpc.key.initialize', [
548484
responseHandler<EnclavedConfig>(async (_req) => {
549485
try {

src/masterBitgoExpress/enclavedExpressClient.ts

Whitespace-only changes.

0 commit comments

Comments
 (0)