Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
90 changes: 89 additions & 1 deletion src/__tests__/api/master/generateWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import nock from 'nock';
import sinon from 'sinon';
import { app as expressApp } from '../../../masterBitGoExpressApp';
import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
import { BitgoMpcGpgPubKeys, Environments } from '@bitgo-beta/sdk-core';
import { BitgoMpcGpgPubKeys, Environments, Wallets } from '@bitgo-beta/sdk-core';
import { BitGoAPI } from '@bitgo-beta/sdk-api';
import * as middleware from '../../../shared/middleware';
import { BitGoRequest } from '../../../types/request';
Expand Down Expand Up @@ -348,6 +348,13 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
});

it('should generate a TSS MPC v1 (EdDSA) wallet with separate backup AWM', async () => {
// No MPCv2 setting for the family: the server still wants MPCv1
nock(bitgoApiUrl)
.persist()
.get('/api/v2/tss/settings')
.query(true)
.reply(200, { coinSettings: {} });

const backupAwmUrl = 'http://backup-awm.invalid';

sinon.restore();
Expand Down Expand Up @@ -652,6 +659,13 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
});

it('should generate a TSS MPC v1 wallet by calling the advanced wallet manager service', async () => {
// No MPCv2 setting for the family: the server still wants MPCv1
nock(bitgoApiUrl)
.persist()
.get('/api/v2/tss/settings')
.query(true)
.reply(200, { coinSettings: {} });

nock(bitgoApiUrl)
.persist()
.get('/api/v1/client/constants')
Expand Down Expand Up @@ -2330,6 +2344,80 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
}
});

describe('EdDSA key generation routing by the tss settings multiSigTypeVersion', () => {
let generateStub: sinon.SinonStub;
let routingAgent: request.SuperAgentTest;

beforeEach(() => {
sinon.restore();
const configWithBackup = makeConfig({
advancedWalletManagerBackupUrl: 'http://backup-awm.invalid',
});
sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => {
(req as BitGoRequest<MasterExpressConfig>).bitgo = bitgo;
(req as BitGoRequest<MasterExpressConfig>).config = configWithBackup;
next();
});
routingAgent = request.agent(expressApp(configWithBackup));
generateStub = sinon
.stub(Wallets.prototype, 'generateWalletWithExternalSigner')
.resolves({ wallet: { toJSON: () => ({ id: 'wallet-id' }) } } as never);
});

const mockTssSettings = (family: string, walletCreationSettings: object) =>
nock(bitgoApiUrl)
.get('/api/v2/tss/settings')
.query({ enterprise: 'test_enterprise' })
.reply(200, { coinSettings: { [family]: { walletCreationSettings } } });

// Only the options handed to the SDK are asserted: the stubbed result is not a full wallet
// response, so the route's response encoding is expected to fail after the SDK call.
const generate = (c = eddsaCoin) =>
routingAgent
.post(`/api/v1/${c}/advancedwallet/generate`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ label: 'test_wallet', enterprise: 'test_enterprise', multisigType: 'tss' });

it('uses the EdDSA MPCv2 callbacks when the setting is MPCv2', async () => {
mockTssSettings('sol', { multiSigTypeVersion: 'MPCv2' });
await generate();

generateStub.calledOnce.should.be.true();
const options = generateStub.firstCall.args[0];
options.eddsaMPCv2Callbacks.should.have.keys(
'initializeCallback',
'round1Callback',
'round2Callback',
'finalizeCallback',
);
options.should.not.have.property('eddsaCallbacks');
});

it('keeps the EdDSA MPCv1 callbacks when the setting is absent', async () => {
mockTssSettings('sol', {});
await generate();

const options = generateStub.firstCall.args[0];
options.should.have.property('eddsaCallbacks');
options.should.not.have.property('eddsaMPCv2Callbacks');
});

it('uses the setting of the coin family only', async () => {
mockTssSettings('eth', { multiSigTypeVersion: 'MPCv2' });
await generate();

generateStub.firstCall.args[0].should.have.property('eddsaCallbacks');
});

it('does not read the setting for ECDSA key generation', async () => {
await generate(ecdsaCoin);

const options = generateStub.firstCall.args[0];
options.should.have.property('ecdsaMPCv2Callbacks');
options.should.not.have.property('eddsaMPCv2Callbacks');
});
});

it('should fail when multisig type is invalid / not provided', async () => {
const response = await agent
.post(`/api/v1/${coin}/advancedwallet/generate`)
Expand Down
36 changes: 31 additions & 5 deletions src/masterBitgoExpress/handlers/handleGenerateWallet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from 'assert';
import { GenerateWalletWithExternalSignerOptions, MPCAlgorithm } from '@bitgo-beta/sdk-core';
import { TssSettings } from '@bitgo/public-types';
import { MasterApiSpecRouteRequest } from '../routers/masterBitGoExpressApiSpec';
import coinFactory from '../../shared/coinFactory';
import { BadRequestError } from '../../shared/errors';
Expand All @@ -8,6 +9,7 @@ import { submitJobViaBridgeClient } from './utils/asyncUtils';
import {
createEcdsaMPCv2KeyGenCallbacks,
createEddsaKeyGenCallbacks,
createEddsaMPCv2KeyGenCallbacks,
createOnchainKeyGenCallback,
} from './walletGenerationCallbacks';

Expand Down Expand Up @@ -54,22 +56,41 @@ export async function handleGenerateWallet(
...req.decoded,
type: 'advanced',
multisigType: isTss ? 'tss' : 'onchain',
...keyGenCallbacks(req, isTss ? baseCoin.getMPCAlgorithm() : undefined),
...(await keyGenCallbacks(req, isTss ? baseCoin.getMPCAlgorithm() : undefined)),
});

return { ...result, wallet: result.wallet.toJSON() };
}

/**
* The MPC version BitGo currently wants for new wallets, read from the same server-side setting
* BitGoJS uses (`/api/v2/tss/settings`, per enterprise and coin family), so moving to MPCv2 only
* is a server-side change.
*/
async function walletCreationMultisigTypeVersion(
req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>,
): Promise<string | undefined> {
const baseCoin = await coinFactory.getCoin(req.decoded.coin, req.bitgo);
const tssSettings: TssSettings = await req.bitgo
.get(req.bitgo.microservicesUrl('/api/v2/tss/settings'))
.query({ enterprise: req.decoded.enterprise })
.result();
return tssSettings.coinSettings[baseCoin.getFamily()]?.walletCreationSettings
?.multiSigTypeVersion;
}

/**
* Picks the AWM key generation callbacks the SDK should drive, based on the coin's MPC algorithm.
* An undefined algorithm means an onchain multisig wallet.
*/
function keyGenCallbacks(
async function keyGenCallbacks(
req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>,
algorithm: MPCAlgorithm | undefined,
): Pick<
GenerateWalletWithExternalSignerOptions,
'createKeychainCallback' | 'ecdsaMPCv2Callbacks' | 'eddsaCallbacks'
): Promise<
Pick<
GenerateWalletWithExternalSignerOptions,
'createKeychainCallback' | 'ecdsaMPCv2Callbacks' | 'eddsaCallbacks' | 'eddsaMPCv2Callbacks'
>
> {
const { awmUserClient, awmBackupClient } = req;
assert(awmUserClient, 'User AWM client not initialized');
Expand All @@ -85,6 +106,11 @@ function keyGenCallbacks(
ecdsaMPCv2Callbacks: createEcdsaMPCv2KeyGenCallbacks(awmUserClient, awmBackupClient),
};
case 'eddsa':
if ((await walletCreationMultisigTypeVersion(req)) === 'MPCv2') {
return {
eddsaMPCv2Callbacks: createEddsaMPCv2KeyGenCallbacks(awmUserClient, awmBackupClient),
};
}
return { eddsaCallbacks: createEddsaKeyGenCallbacks(awmUserClient, awmBackupClient) };
default:
throw new BadRequestError(
Expand Down
Loading