Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ coverage/
logs/
tsconfig.tsbuildinfo
out/
.vscode/
299 changes: 299 additions & 0 deletions src/__tests__/masterBitgoExpress/.wip.md

Large diffs are not rendered by default.

130 changes: 130 additions & 0 deletions src/__tests__/masterBitgoExpress/generateWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('POST /api/:coin/wallet/generate', () => {
const enclavedExpressUrl = 'http://enclaved.invalid';
const bitgoApiUrl = Environments.test.uri;
const coin = 'tbtc';
const eddsaCoin = 'tsol';
const accessToken = 'test-token';

before(() => {
Expand Down Expand Up @@ -136,6 +137,135 @@ describe('POST /api/:coin/wallet/generate', () => {
bitgoAddWalletNock.done();
});

it('should generate a TSS wallet by calling the enclaved express service', async () => {
const userInitNock = nock(enclavedExpressUrl)
.post(`/api/${eddsaCoin}/mpc/initialize`, {
source: 'user',
})
.reply(200, {
encryptedDataKey: 'key',
encryptedData: 'data',
bitgoPayload: {
from: 'user',
to: 'bitgo',
publicShare:
'dcf591bfb22f9764ed382dcb397f591bdb64c69773c6cf2902d14789a13811a0a768fb0eae38f9ebe2b047182e2a95bb49921bfec56bcd96e3075e53396c1775',
privateShare:
'175bdf3264662e1d13de1dacc22c0913b367f165fb15439fe687cbdc1713560ca768fb0eae38f9ebe2b047182e2a95bb49921bfec56bcd96e3075e53396c1775',
privateShareProof:
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwTGXYL4mPPKg3u1KkPeXR9lOqqem/i3kgdgQE9P\nIZlvNdZyVcoAyrTos0Negm39jQPzssKbjNYbwmD6oBliJIWDzVUxYzU3NDY3\nNmUwNWM3Zjc0Zjg4YmM5YmEgPHVzZXItMWM1NzQ2NzZlMDVjN2Y3NGY4OGJj\nOWJhQDFjNTc0Njc2ZTA1YzdmNzRmODhiYzliYS5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQ6ylVI/YkWEQDFQgKBBYAAgECGQECmwMCHgEWIQRS2wpzMoJX\nVNidgnnrKVUj9iRYRAAA0kkA/R78hy0CNnUPCMMi2Co6VlYALrx+xFydb0+7\n8Yza5IF2AP93Xc9FKo8OPO5pg5uPnC6fXvsJqVne289iETTtsihaaM5TBGhU\nZoASBSuBBAAKAgME99PyPC8OyvjMb5GMLIvU3UOa8vDHDw4EJxEk9vjP1M8w\n9Uz8BlRby1wYFShcTYrl8lqBmvO9KswHXSLvwyw1QAMBCAfCeAQYEwgAKgWC\naFRmgAmQ6ylVI/YkWEQCmwwWIQRS2wpzMoJXVNidgnnrKVUj9iRYRAAASxsA\n/RbBP5LPbfcAay8osipVWf/oTzw2/tKzER0K3FfAAsImAP0c2ee+qa0Tn5nv\neezRo+XxgIoxw2gT8jYpyzJw+BKBBQ==\n=DYMw\n-----END PGP PUBLIC KEY BLOCK-----\n',
vssProof: '011532df3eceab48fc91c2e17e7accea1d0dd30b8b7562a5f602afb2130ab26a',
userGPGPublicKey:
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwTGXYL4mPPKg3u1KkPeXR9lOqqem/i3kgdgQE9P\nIZlvNdZyVcoAyrTos0Negm39jQPzssKbjNYbwmD6oBliJIWDzVUxYzU3NDY3\nNmUwNWM3Zjc0Zjg4YmM5YmEgPHVzZXItMWM1NzQ2NzZlMDVjN2Y3NGY4OGJj\nOWJhQDFjNTc0Njc2ZTA1YzdmNzRmODhiYzliYS5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQ6ylVI/YkWEQDFQgKBBYAAgECGQECmwMCHgEWIQRS2wpzMoJX\nVNidgnnrKVUj9iRYRAAA0kkA/R78hy0CNnUPCMMi2Co6VlYALrx+xFydb0+7\n8Yza5IF2AP93Xc9FKo8OPO5pg5uPnC6fXvsJqVne289iETTtsihaaM5TBGhU\nZoASBSuBBAAKAgME99PyPC8OyvjMb5GMLIvU3UOa8vDHDw4EJxEk9vjP1M8w\n9Uz8BlRby1wYFShcTYrl8lqBmvO9KswHXSLvwyw1QAMBCAfCeAQYEwgAKgWC\naFRmgAmQ6ylVI/YkWEQCmwwWIQRS2wpzMoJXVNidgnnrKVUj9iRYRAAASxsA\n/RbBP5LPbfcAay8osipVWf/oTzw2/tKzER0K3FfAAsImAP0c2ee+qa0Tn5nv\neezRo+XxgIoxw2gT8jYpyzJw+BKBBQ==\n=DYMw\n-----END PGP PUBLIC KEY BLOCK-----\n',
},
});

const backupInitNock = nock(enclavedExpressUrl)
.post(`/api/${eddsaCoin}/mpc/initialize`, {
source: 'backup',
})
.reply(200, {
encryptedDataKey: 'key',
encryptedData: 'data',
bitgoPayload: {
from: 'backup',
to: 'bitgo',
publicShare:
'280b5d3b40899e6e1cac86906602ffdf76b70aefc2def7f311693aba654cca6ecdcb2be051910ebc9bcbae6ac0db3edf707498b19be0f229102ce76dd880ab9b',
privateShare:
'1be0dcb0b3c77bceac11ce77d83b33a5b74ff39f90485d81b2003bc55270b509cdcb2be051910ebc9bcbae6ac0db3edf707498b19be0f229102ce76dd880ab9b',
privateShareProof:
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwQbnZsAMbrZ6LnlMT8ZjmCyq4Au+KDEMH9dndk5\nqVpZIgvHzMwZYusZtija5M/erWbg0Iutv1R1olMd9htHSScOzVViMmZlNTRl\nZTI1YzIyOWM0MzJiNzU2MWYgPHVzZXItYjJmZTU0ZWUyNWMyMjljNDMyYjc1\nNjFmQGIyZmU1NGVlMjVjMjI5YzQzMmI3NTYxZi5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQrNctBNmaAcADFQgKBBYAAgECGQECmwMCHgEWIQSJSRD0FwPm\nwraqiESs1y0E2ZoBwAAAqJkBAIhIhHS8i71tbe43TKYThRaOzeo73afL31UE\nbK12huloAQCrjr5GEz+4L84Nl8TcWt5yAI8UF1hi+O5rdP35UL6xKc5TBGhU\nZoASBSuBBAAKAgME+Bm/MFl4fP7CxJsannVVcZ1M+bL8X8kcl30wXaLkiqvg\nZpEunra42o4RwaQcQirsvPX9+di0P2FoFXH/n1+s1wMBCAfCeAQYEwgAKgWC\naFRmgAmQrNctBNmaAcACmwwWIQSJSRD0FwPmwraqiESs1y0E2ZoBwAAAXWoB\nAI9xw2J9mzyPGpnFiIb/qxHRzSbXsNYyPvxUU15rSKiaAP9uy61NJBs3vTT8\nzf33PkAgoxFZsEDLwAsDyOecH/Cilw==\n=0SE8\n-----END PGP PUBLIC KEY BLOCK-----\n',
vssProof: 'f008212df4a14e81b8b7bca268a3b2b19d65220fb2f0b2e1c8f83e0d9286aec2',
backupGPGPublicKey:
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwQbnZsAMbrZ6LnlMT8ZjmCyq4Au+KDEMH9dndk5\nqVpZIgvHzMwZYusZtija5M/erWbg0Iutv1R1olMd9htHSScOzVViMmZlNTRl\nZTI1YzIyOWM0MzJiNzU2MWYgPHVzZXItYjJmZTU0ZWUyNWMyMjljNDMyYjc1\nNjFmQGIyZmU1NGVlMjVjMjI5YzQzMmI3NTYxZi5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQrNctBNmaAcADFQgKBBYAAgECGQECmwMCHgEWIQSJSRD0FwPm\nwraqiESs1y0E2ZoBwAAAqJkBAIhIhHS8i71tbe43TKYThRaOzeo73afL31UE\nbK12huloAQCrjr5GEz+4L84Nl8TcWt5yAI8UF1hi+O5rdP35UL6xKc5TBGhU\nZoASBSuBBAAKAgME+Bm/MFl4fP7CxJsannVVcZ1M+bL8X8kcl30wXaLkiqvg\nZpEunra42o4RwaQcQirsvPX9+di0P2FoFXH/n1+s1wMBCAfCeAQYEwgAKgWC\naFRmgAmQrNctBNmaAcACmwwWIQSJSRD0FwPmwraqiESs1y0E2ZoBwAAAXWoB\nAI9xw2J9mzyPGpnFiIb/qxHRzSbXsNYyPvxUU15rSKiaAP9uy61NJBs3vTT8\nzf33PkAgoxFZsEDLwAsDyOecH/Cilw==\n=0SE8\n-----END PGP PUBLIC KEY BLOCK-----\n',
},
});

const bitgoAddKeychainNock = nock(bitgoApiUrl)
.post(`/api/v2/${eddsaCoin}/key`)
.reply(function (uri, requestBody) {
// Verify request structure
const body = requestBody as any;
body.should.have.properties({
keyType: 'tss',
source: 'bitgo',
enterprise: 'test_enterprise',
});

// Verify key shares structure
body.should.have.property('keyShares').which.is.an.Array().of.length(2);

// Verify user share
const userShare = body.keyShares.find((s: any) => s.from === 'user' && s.to === 'bitgo');
userShare.should.have.properties([
'publicShare',
'privateShare',
'privateShareProof',
'vssProof',
]);
userShare.publicShare.should.be.a.String().and.not.empty();
userShare.privateShare.should.be.a.String().and.not.empty();
userShare.privateShareProof.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
userShare.vssProof.should.be.a.String().and.not.empty();

// Verify backup share
const backupShare = body.keyShares.find(
(s: any) => s.from === 'backup' && s.to === 'bitgo',
);
backupShare.should.have.properties([
'publicShare',
'privateShare',
'privateShareProof',
'vssProof',
]);
backupShare.publicShare.should.be.a.String().and.not.empty();
backupShare.privateShare.should.be.a.String().and.not.empty();
backupShare.privateShareProof.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
backupShare.vssProof.should.be.a.String().and.not.empty();

// Verify GPG keys
body.userGPGPublicKey.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
body.backupGPGPublicKey.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');

return [
200,
{
id: 'bitgo-key-id',
commonKeychain:
'4e534a0193c6636a0727079e25601abd6c2853d63582162bc53ae69b152f0ec2c2e096583da8e7ffd36dff6131a17020727f9543001525c172c1e772900359d3',
},
];
});

const response = await agent
.post(`/api/${eddsaCoin}/wallet/generate`)
.set('Authorization', `Bearer ${accessToken}`)
.send({
label: 'test_wallet',
enterprise: 'test_enterprise',
multisigType: 'tss',
});

// Verify response status and structure
response.status.should.equal(500); // TODO: Update to 200 when fully integrated with finalize endpoint
// response.body.should.have.property('bitgoKeychain');
//
// // Verify BitGo keychain properties
// const bitgoKeychain = response.body.bitgoKeychain;
// bitgoKeychain.should.have.property('id').which.is.a.String();
// bitgoKeychain.should.have.property('commonKeychain').which.is.a.String();
// bitgoKeychain.id.should.equal('bitgo-key-id');
// bitgoKeychain.commonKeychain.should.equal(
// '4e534a0193c6636a0727079e25601abd6c2853d63582162bc53ae69b152f0ec2c2e096583da8e7ffd36dff6131a17020727f9543001525c172c1e772900359d3',
// );

// Verify all nock mocks were called
userInitNock.done();
backupInitNock.done();
bitgoAddKeychainNock.done();
});

it('should fail when enclaved express client is not configured', async () => {
// Create a config without enclaved express settings
const invalidConfig: Partial<MasterExpressConfig> = {
Expand Down
111 changes: 111 additions & 0 deletions src/enclavedBitgoExpress/routers/enclavedApiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,72 @@ import { signMultisigTransaction } from '../../api/enclaved/signMultisigTransact
import { prepareBitGo, responseHandler } from '../../shared/middleware';
import { EnclavedConfig } from '../../types';
import { BitGoRequest } from '../../types/request';
import { NotImplementedError } from 'bitgo';

Copilot AI Jun 19, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Importing NotImplementedError from 'bitgo' may be inconsistent with other modules using @bitgo/sdk-core; align the import source to avoid mismatches.

Suggested change
import { NotImplementedError } from 'bitgo';
import { NotImplementedError } from '@bitgo/sdk-core';

Copilot uses AI. Check for mistakes.

// Request type for /key/independent endpoint
const IndependentKeyRequest = {
source: t.string,
seed: t.union([t.undefined, t.string]),
};

const BitgoPayloadType = t.union([
t.type({
from: t.literal('user'),
to: t.literal('bitgo'),
publicShare: t.string,
privateShare: t.string,
privateShareProof: t.string,
vssProof: t.string,
userGPGPublicKey: t.string,
}),
t.type({
from: t.literal('backup'),
to: t.literal('bitgo'),
publicShare: t.string,
privateShare: t.string,
privateShareProof: t.string,
vssProof: t.string,
backupGPGPublicKey: t.string,
}),
]);

export const InitEddsaKeyGenerationRequest = {
source: t.union([t.literal('user'), t.literal('backup')]),
};

export const InitEddsaKeyGenerationResponse = t.type({
encryptedDataKey: t.string,
encryptedData: t.string,
bitgoPayload: BitgoPayloadType,
});

export type InitEddsaKeyGenerationResponse = t.TypeOf<typeof InitEddsaKeyGenerationResponse>;

// Types for /mpc/finalize endpoint
const BitGoKeychainType = t.type({
id: t.string,
source: t.literal('bitgo'),
type: t.literal('tss'),
commonKeychain: t.string,
verifiedVssProof: t.boolean,
isBitGo: t.boolean,
isTrust: t.boolean,
hsmType: t.string,
});

const FinalizeKeyGenerationRequest = {
encryptedDataKey: t.string,
encryptedData: t.string,
bitGoKeychain: BitGoKeychainType,
source: t.union([t.literal('user'), t.literal('backup')]),
};

const FinalizeKeyGenerationResponse = t.type({
commonKeychain: t.string,
enclavedExpressKeyId: t.string,
source: t.union([t.literal('user'), t.literal('backup')]),
});

// Response type for /key/independent endpoint
const IndependentKeyResponse: HttpResponse = {
// TODO: Define proper response type
Expand Down Expand Up @@ -123,6 +182,46 @@ export const EnclavedAPiSpec = apiSpec({
description: 'Generate an independent key',
}),
},
'v1.key.mpc.init': {
post: httpRoute({
method: 'POST',
path: '/api/{coin}/mpc/initialize',
request: httpRequest({
params: {
coin: t.string,
},
body: InitEddsaKeyGenerationRequest,
}),
response: {
200: InitEddsaKeyGenerationResponse,
500: t.type({
error: t.string,
details: t.string,
}),
},
description: 'Initialize Eddsa key generation',
}),
},
'v1.mpc.finalize': {
post: httpRoute({
method: 'POST',
path: '/api/{coin}/mpc/finalize',
request: httpRequest({
params: {
coin: t.string,
},
body: FinalizeKeyGenerationRequest,
}),
response: {
200: FinalizeKeyGenerationResponse,
500: t.type({
error: t.string,
details: t.string,
}),
},
description: 'Finalize key generation and confirm commonKeychain',
}),
},
});

export type EnclavedApiSpecRouteHandler<
Expand Down Expand Up @@ -170,5 +269,17 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter<typeof
}),
]);

router.post('v1.key.mpc.init', [
responseHandler<EnclavedConfig>(async (_req) => {
throw new NotImplementedError('MPC key generation is not implemented yet');
}),
]);

router.post('v1.mpc.finalize', [
responseHandler<EnclavedConfig>(async (_req) => {
throw new NotImplementedError('MPC key finalization is not implemented yet');
}),
]);

return router;
}
84 changes: 84 additions & 0 deletions src/masterBitgoExpress/enclavedExpressClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,32 @@ import { MasterExpressConfig } from '../types';
import { TlsMode } from '../types';
import { EnclavedApiSpec } from '../enclavedBitgoExpress/routers';
import { PingResponseType, VersionResponseType } from '../types/health';
import { InitEddsaKeyGenerationResponse } from '../enclavedBitgoExpress/routers/enclavedApiSpec';

const debugLogger = debug('bitgo:express:enclavedExpressClient');

interface InitMpcKeyGenerationParams {
source: 'user' | 'backup';
coin?: string;
}

interface FinalizeMpcKeyGenerationParams {
source: 'user' | 'backup';
coin?: string;
encryptedDataKey: string;
encryptedData: string;
bitGoKeychain: {
id: string;
source: 'bitgo';
type: 'tss';
commonKeychain: string;
verifiedVssProof: boolean;
isBitGo: boolean;
isTrust: boolean;
hsmType: string;
};
}

interface CreateIndependentKeychainParams {
source: 'user' | 'backup';
coin?: string;
Expand Down Expand Up @@ -229,6 +252,67 @@ export class EnclavedExpressClient {
throw err;
}
}

/**
* Initialize MPC key generation for a given source and coin
*/
async initMpcKeyGeneration(
params: InitMpcKeyGenerationParams,
): Promise<InitEddsaKeyGenerationResponse> {
if (!this.coin) {
throw new Error('Coin must be specified to initialize MPC key generation');
}

try {
debugLogger('Initializing MPC key generation for coin: %s', this.coin);
let request = this.apiClient['v1.key.mpc.init'].post({
coin: this.coin,
source: params.source,
});

if (this.tlsMode === TlsMode.MTLS) {
request = request.agent(this.createHttpsAgent());
}

const response = await request.decodeExpecting(200);
return response.body;
} catch (error) {
const err = error as Error;
debugLogger('Failed to initialize MPC key generation: %s', err.message);
throw err;
}
}

/**
* Finalize MPC key generation for a given source and coin
*/
async finalizeMpcKeyGeneration(params: FinalizeMpcKeyGenerationParams): Promise<any> {
if (!this.coin) {
throw new Error('Coin must be specified to finalize MPC key generation');
}

try {
debugLogger('Finalizing MPC key generation for coin: %s', this.coin);
let request = this.apiClient['v1.mpc.finalize'].post({
coin: this.coin,
source: params.source,
encryptedDataKey: params.encryptedDataKey,
encryptedData: params.encryptedData,
bitGoKeychain: params.bitGoKeychain,
});

if (this.tlsMode === TlsMode.MTLS) {
request = request.agent(this.createHttpsAgent());
}

const response = await request.decodeExpecting(200);
return response.body;
} catch (error) {
const err = error as Error;
debugLogger('Failed to finalize MPC key generation: %s', err.message);
throw err;
}
}
}

/**
Expand Down
Loading
Loading