Skip to content

Commit 2bb7ce9

Browse files
feat(mbe): consolidation eddsa
1 parent 8154e4a commit 2bb7ce9

File tree

5 files changed

+248
-17
lines changed

5 files changed

+248
-17
lines changed

masterBitgoExpress.json

Lines changed: 117 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,120 @@
66
"description": "BitGo Enclaved Express - Secure enclave for BitGo signing operations with mTLS"
77
},
88
"paths": {
9+
"/api/{coin}/wallet/{walletId}/accelerate": {
10+
"post": {
11+
"parameters": [
12+
{
13+
"name": "walletId",
14+
"in": "path",
15+
"required": true,
16+
"schema": {
17+
"type": "string"
18+
}
19+
},
20+
{
21+
"name": "coin",
22+
"in": "path",
23+
"required": true,
24+
"schema": {
25+
"type": "string"
26+
}
27+
}
28+
],
29+
"requestBody": {
30+
"content": {
31+
"application/json": {
32+
"schema": {
33+
"type": "object",
34+
"properties": {
35+
"pubkey": {
36+
"type": "string"
37+
},
38+
"source": {
39+
"type": "string",
40+
"enum": [
41+
"user",
42+
"backup"
43+
]
44+
},
45+
"cpfpTxIds": {
46+
"type": "array",
47+
"items": {
48+
"type": "string"
49+
}
50+
},
51+
"cpfpFeeRate": {
52+
"type": "number"
53+
},
54+
"maxFee": {
55+
"type": "number"
56+
},
57+
"rbfTxIds": {
58+
"type": "array",
59+
"items": {
60+
"type": "string"
61+
}
62+
},
63+
"feeMultiplier": {
64+
"type": "number"
65+
}
66+
},
67+
"required": [
68+
"pubkey",
69+
"source"
70+
]
71+
}
72+
}
73+
}
74+
},
75+
"responses": {
76+
"200": {
77+
"description": "OK",
78+
"content": {
79+
"application/json": {
80+
"schema": {
81+
"type": "object",
82+
"properties": {
83+
"txid": {
84+
"type": "string"
85+
},
86+
"tx": {
87+
"type": "string"
88+
}
89+
},
90+
"required": [
91+
"txid",
92+
"tx"
93+
]
94+
}
95+
}
96+
}
97+
},
98+
"500": {
99+
"description": "Internal Server Error",
100+
"content": {
101+
"application/json": {
102+
"schema": {
103+
"type": "object",
104+
"properties": {
105+
"error": {
106+
"type": "string"
107+
},
108+
"details": {
109+
"type": "string"
110+
}
111+
},
112+
"required": [
113+
"error",
114+
"details"
115+
]
116+
}
117+
}
118+
}
119+
}
120+
}
121+
}
122+
},
9123
"/api/{coin}/wallet/{walletId}/consolidate": {
10124
"post": {
11125
"parameters": [
@@ -54,6 +168,9 @@
54168
"full",
55169
"lite"
56170
]
171+
},
172+
"commonKeychain": {
173+
"type": "string"
57174
}
58175
},
59176
"required": [
@@ -142,9 +259,6 @@
142259
"backup"
143260
]
144261
},
145-
"walletPassphrase": {
146-
"type": "string"
147-
},
148262
"feeRate": {
149263
"type": "number"
150264
},

src/api/master/handlerUtils.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export async function getWalletAndSigningKeychain({
1717
bitgo: BitGo;
1818
coin: string;
1919
walletId: string;
20-
params: { source: 'user' | 'backup'; pubkey?: string };
20+
params: { source: 'user' | 'backup'; pubkey?: string; commonKeychain?: string };
2121
reqId: RequestTracer;
2222
KeyIndices: { USER: number; BACKUP: number; BITGO: number };
2323
}) {
@@ -34,14 +34,20 @@ export async function getWalletAndSigningKeychain({
3434
id: wallet.keyIds()[keyIdIndex],
3535
});
3636

37-
if (!signingKeychain || !signingKeychain.pub) {
37+
if (!signingKeychain) {
3838
throw new Error(`Signing keychain for ${params.source} not found`);
3939
}
4040

4141
if (params.pubkey && params.pubkey !== signingKeychain.pub) {
4242
throw new Error(`Pub provided does not match the keychain on wallet for ${params.source}`);
4343
}
4444

45+
if (params.commonKeychain && signingKeychain.commonKeychain !== params.commonKeychain) {
46+
throw new Error(
47+
`Common keychain provided does not match the keychain on wallet for ${params.source}`,
48+
);
49+
}
50+
4551
return { baseCoin, wallet, signingKeychain };
4652
}
4753
/**

src/api/master/handlers/eddsa.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import {
99
EddsaUtils,
1010
BaseCoin,
1111
ApiKeyShare,
12+
CustomRShareGeneratingFunction,
13+
CustomGShareGeneratingFunction,
14+
CustomCommitmentGeneratingFunction,
1215
} from '@bitgo/sdk-core';
1316
import { EnclavedExpressClient } from '../clients/enclavedExpressClient';
1417
import { exchangeEddsaCommitments } from '@bitgo/sdk-core/dist/src/bitgo/tss/common';
@@ -198,3 +201,70 @@ export async function orchestrateEddsaKeyGen({
198201
walletParams.keys = [userMpcKey.id, backupMpcKey.id, bitgoKeychain.id];
199202
return { walletParams, keychains };
200203
}
204+
205+
// Commitment
206+
export function createCustomCommitmentGenerator(
207+
bitgo: BitGoBase,
208+
wallet: Wallet,
209+
enclavedExpressClient: EnclavedExpressClient,
210+
source: 'user' | 'backup',
211+
pub: string,
212+
): CustomCommitmentGeneratingFunction {
213+
return async function customCommitmentGeneratingFunction(params) {
214+
const eddsaUtils = new EddsaUtils(bitgo, wallet.baseCoin);
215+
const bitgoGpgKey = await eddsaUtils.getBitgoPublicGpgKey();
216+
const { txRequest } = params;
217+
const response = await enclavedExpressClient.signMpcCommitment({
218+
txRequest,
219+
bitgoGpgPubKey: bitgoGpgKey.armor(),
220+
source,
221+
pub,
222+
});
223+
return {
224+
...response,
225+
encryptedUserToBitgoRShare: {
226+
...response.encryptedUserToBitgoRShare,
227+
encryptedDataKey: response.encryptedDataKey,
228+
},
229+
};
230+
};
231+
}
232+
233+
// RShare
234+
export function createCustomRShareGenerator(
235+
enclavedExpressClient: EnclavedExpressClient,
236+
source: 'user' | 'backup',
237+
pub: string,
238+
): CustomRShareGeneratingFunction {
239+
return async function customRShareGeneratingFunction(params) {
240+
const { txRequest, encryptedUserToBitgoRShare } = params;
241+
const encryptedDataKey = (encryptedUserToBitgoRShare as any).encryptedDataKey;
242+
return await enclavedExpressClient.signMpcRShare({
243+
txRequest,
244+
encryptedUserToBitgoRShare,
245+
encryptedDataKey,
246+
source,
247+
pub,
248+
});
249+
};
250+
}
251+
252+
// GShare
253+
export function createCustomGShareGenerator(
254+
enclavedExpressClient: EnclavedExpressClient,
255+
source: 'user' | 'backup',
256+
pub: string,
257+
): CustomGShareGeneratingFunction {
258+
return async function customGShareGeneratingFunction(params) {
259+
const { txRequest, bitgoToUserRShare, userToBitgoRShare, bitgoToUserCommitment } = params;
260+
const response = await enclavedExpressClient.signMpcGShare({
261+
txRequest,
262+
bitgoToUserRShare,
263+
userToBitgoRShare,
264+
bitgoToUserCommitment,
265+
source,
266+
pub,
267+
});
268+
return response.gShare;
269+
};
270+
}

src/api/master/handlers/handleConsolidate.ts

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
1-
import { RequestTracer, KeyIndices } from '@bitgo/sdk-core';
1+
import {
2+
RequestTracer,
3+
KeyIndices,
4+
BuildConsolidationTransactionOptions,
5+
MPCType,
6+
} from '@bitgo/sdk-core';
27
import logger from '../../../logger';
38
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
49
import { getWalletAndSigningKeychain, makeCustomSigningFunction } from '../handlerUtils';
10+
import {
11+
createCustomCommitmentGenerator,
12+
createCustomRShareGenerator,
13+
createCustomGShareGenerator,
14+
} from './eddsa';
515

616
export async function handleConsolidate(
717
req: MasterApiSpecRouteRequest<'v1.wallet.consolidate', 'post'>,
@@ -33,20 +43,50 @@ export async function handleConsolidate(
3343
}
3444

3545
try {
36-
// Create custom signing function that delegates to EBE
37-
const customSigningFunction = makeCustomSigningFunction({
38-
enclavedExpressClient,
39-
source: params.source,
40-
pub: signingKeychain.pub!,
41-
});
42-
43-
// Prepare consolidation parameters
44-
const consolidationParams = {
46+
const consolidationParams: BuildConsolidationTransactionOptions = {
4547
...params,
46-
customSigningFunction,
4748
reqId,
4849
};
4950

51+
// --- TSS/MPC support ---
52+
if (wallet._wallet.multisigType === 'tss') {
53+
// Always force apiVersion to 'full' for TSS/MPC
54+
consolidationParams.apiVersion = 'full';
55+
56+
// EDDSA (e.g., Solana)
57+
if (baseCoin.getMPCAlgorithm() === MPCType.EDDSA) {
58+
consolidationParams.customCommitmentGeneratingFunction = createCustomCommitmentGenerator(
59+
bitgo,
60+
wallet,
61+
enclavedExpressClient,
62+
params.source,
63+
signingKeychain.commonKeychain!,
64+
);
65+
consolidationParams.customRShareGeneratingFunction = createCustomRShareGenerator(
66+
enclavedExpressClient,
67+
params.source,
68+
signingKeychain.commonKeychain!,
69+
);
70+
consolidationParams.customGShareGeneratingFunction = createCustomGShareGenerator(
71+
enclavedExpressClient,
72+
params.source,
73+
signingKeychain.commonKeychain!,
74+
);
75+
}
76+
// ECDSA (future-proof, not needed for Solana)
77+
else if (baseCoin.getMPCAlgorithm() === MPCType.ECDSA) {
78+
// Add ECDSA custom signing hooks here if needed, following SDK pattern
79+
throw new Error('ECDSA MPC consolidations not yet implemented');
80+
}
81+
} else {
82+
// Non-TSS: legacy custom signing function
83+
consolidationParams.customSigningFunction = makeCustomSigningFunction({
84+
enclavedExpressClient,
85+
source: params.source,
86+
pub: signingKeychain.pub!,
87+
});
88+
}
89+
5090
// Send account consolidations
5191
const result = await wallet.sendAccountConsolidations(consolidationParams);
5292

src/api/master/routers/masterApiSpec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,11 @@ export const SendManyResponse: HttpResponse = {
106106

107107
// Request type for /consolidate endpoint
108108
export const ConsolidateRequest = {
109-
pubkey: t.string,
109+
pubkey: t.union([t.undefined, t.string]),
110110
source: t.union([t.literal('user'), t.literal('backup')]),
111111
consolidateAddresses: t.union([t.undefined, t.array(t.string)]),
112112
apiVersion: t.union([t.undefined, t.literal('full'), t.literal('lite')]),
113+
commonKeychain: t.union([t.undefined, t.string]),
113114
};
114115

115116
// Response type for /consolidate endpoint

0 commit comments

Comments
 (0)