Skip to content

Commit

Permalink
feat(sdk-core): add MPCv2 wallet creation
Browse files Browse the repository at this point in the history
added MPCv2 wallet creation

WP-1686

TICKET: WP-1686
  • Loading branch information
alebusse committed Apr 18, 2024
1 parent 726379c commit 4eaf7ff
Show file tree
Hide file tree
Showing 17 changed files with 1,189 additions and 86 deletions.
427 changes: 427 additions & 0 deletions modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2.ts

Large diffs are not rendered by default.

109 changes: 104 additions & 5 deletions modules/bitgo/test/v2/unit/wallets.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
//
// Tests for Wallets
//

import * as assert from 'assert';
import * as nock from 'nock';
import * as sinon from 'sinon';
import * as should from 'should';
import * as _ from 'lodash';
import { TestBitGo } from '@bitgo/sdk-test';
import {
BlsUtils,
common,
Expand All @@ -12,11 +16,7 @@ import {
KeychainsTriplet,
GenerateWalletOptions,
} from '@bitgo/sdk-core';
import * as _ from 'lodash';
import { TestBitGo } from '@bitgo/sdk-test';
import { BitGo } from '../../../src/bitgo';
import * as sinon from 'sinon';
import * as should from 'should';

describe('V2 Wallets:', function () {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
Expand Down Expand Up @@ -702,6 +702,105 @@ describe('V2 Wallets:', function () {
});
});

describe('Generate TSS MPCv2 wallet:', async function () {
it('should create a new TSS MPCv2 wallet', async function () {
const hteth = bitgo.coin('hteth');
const sandbox = sinon.createSandbox();
const stubbedKeychainsTriplet: KeychainsTriplet = {
userKeychain: {
id: '1',
commonKeychain: 'userPub',
type: 'tss',
source: 'user',
},
backupKeychain: {
id: '2',
commonKeychain: 'userPub',
type: 'tss',
source: 'backup',
},
bitgoKeychain: {
id: '3',
commonKeychain: 'userPub',
type: 'tss',
source: 'bitgo',
},
};
sandbox.stub(ECDSAUtils.EcdsaMPCv2Utils.prototype, 'createKeychains').resolves(stubbedKeychainsTriplet);

const walletNock = nock('https://bitgo.fakeurl').post('/api/v2/hteth/wallet').reply(200);

const wallets = new Wallets(bitgo, hteth);

await wallets.generateWallet({
label: 'tss wallet',
passphrase: 'tss password',
multisigType: 'tss',
enterprise: 'enterprise',
passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
walletVersion: 5,
});

walletNock.isDone().should.be.true();
sandbox.verifyAndRestore();
});

it('should throw for an unsupported coin', async function () {
const tpolygon = bitgo.coin('tpolygon');
const wallets = new Wallets(bitgo, tpolygon);

await assert.rejects(
async () => {
await wallets.generateWallet({
label: 'tss wallet',
passphrase: 'tss password',
multisigType: 'tss',
enterprise: 'enterprise',
passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
walletVersion: 5,
});
},
{ message: 'coin polygon does not support TSS MPCv2 at this time' }
);
});

it('should throw for a cold wallet', async function () {
const hteth = bitgo.coin('hteth');
const wallets = new Wallets(bitgo, hteth);

await assert.rejects(
async () => {
await wallets.generateWallet({
label: 'tss wallet',
multisigType: 'tss',
enterprise: 'enterprise',
walletVersion: 5,
type: 'cold',
});
},
{ message: 'EVM TSS MPCv2 wallets are not supported for cold wallets' }
);
});

it('should throw for a custodial wallet', async function () {
const hteth = bitgo.coin('hteth');
const wallets = new Wallets(bitgo, hteth);

await assert.rejects(
async () => {
await wallets.generateWallet({
label: 'tss wallet',
multisigType: 'tss',
enterprise: 'enterprise',
walletVersion: 5,
type: 'custodial',
});
},
{ message: 'EVM TSS MPCv2 wallets are not supported for custodial wallets' }
);
});
});

describe('Generate BLS-DKG wallet:', function () {
const eth2 = bitgo.coin('eth2');

Expand Down
3 changes: 2 additions & 1 deletion modules/sdk-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
},
"dependencies": {
"@bitgo/bls-dkg": "^1.3.1",
"@bitgo/public-types": "2.1.0",
"@bitgo/public-types": "2.7.0",
"@bitgo/sdk-lib-mpc": "^9.3.0",
"@bitgo/statics": "^48.7.0",
"@bitgo/utxo-lib": "^9.35.0",
Expand All @@ -59,6 +59,7 @@
"ethereumjs-util": "7.1.5",
"fp-ts": "^2.12.2",
"io-ts": "2.1.3",
"io-ts-types": "0.5.16",
"keccak": "3.0.3",
"libsodium-wrappers-sumo": "^0.7.9",
"lodash": "^4.17.15",
Expand Down
10 changes: 10 additions & 0 deletions modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { bip32 } from '@bitgo/utxo-lib';
import { BigNumber } from 'bignumber.js';

import * as utxolib from '@bitgo/utxo-lib';
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';

import { InitiateRecoveryOptions } from '../recovery';
import { signMessage } from '../bip32util';
Expand Down Expand Up @@ -55,6 +56,7 @@ export abstract class BaseCoin implements IBaseCoin {
protected readonly _pendingApprovals: PendingApprovals;
protected readonly _markets: Markets;
protected static readonly _coinTokenPatternSeparator = ':';
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;

protected constructor(bitgo: BitGoBase) {
this.bitgo = bitgo;
Expand Down Expand Up @@ -103,6 +105,14 @@ export abstract class BaseCoin implements IBaseCoin {
return this.getChain();
}

/**
* Gets the statics coin object
* @returns {Readonly<StaticsBaseCoin>} the statics coin object
*/
getConfig(): Readonly<StaticsBaseCoin> {
return this._staticsCoin;
}

/**
* Name of the chain which supports this coin (eg, 'btc', 'eth')
*/
Expand Down
3 changes: 2 additions & 1 deletion modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import BigNumber from 'bignumber.js';
import { BaseCoin as StaticsBaseCoin, BaseTokenConfig } from '@bitgo/statics';
import { IRequestTracer } from '../../api';
import { IEnterprises } from '../enterprise';
import { Keychain, IKeychains } from '../keychain';
Expand All @@ -10,7 +11,6 @@ import EddsaUtils, { TxRequest } from '../utils/tss/eddsa';
import { CustomSigningFunction, IWallet, IWallets, Wallet, WalletData } from '../wallet';

import { IWebhooks } from '../webhook/iWebhooks';
import { BaseTokenConfig } from '@bitgo/statics';
import { TransactionType } from '../../account-lib';
import { IInscriptionBuilder } from '../inscriptionBuilder';
import { Hash } from 'crypto';
Expand Down Expand Up @@ -448,6 +448,7 @@ export interface BaseBroadcastTransactionResult {
export interface IBaseCoin {
type: string;
tokenConfig?: BaseTokenConfig;
getConfig(): Readonly<StaticsBaseCoin>;
url(suffix: string): string;
wallets(): IWallets;
enterprises(): IEnterprises;
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-core/src/bitgo/keychain/iKeychains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export interface CreateMpcOptions {
originalPasscodeEncryptionCode?: string;
enterprise?: string;
backupProvider?: BackupProvider;
walletVersion?: number;
}

export interface GetKeysForSigningOptions {
Expand Down
7 changes: 6 additions & 1 deletion modules/sdk-core/src/bitgo/keychain/keychains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,12 @@ export class Keychains implements IKeychains {
let MpcUtils;
switch (params.multisigType) {
case 'tss':
MpcUtils = this.baseCoin.getMPCAlgorithm() === 'ecdsa' ? ECDSAUtils.EcdsaUtils : EDDSAUtils.default;
MpcUtils =
this.baseCoin.getMPCAlgorithm() === 'eddsa'
? EDDSAUtils.default
: params.walletVersion === 5
? ECDSAUtils.EcdsaMPCv2Utils
: ECDSAUtils.EcdsaUtils;
break;
case 'blsdkg':
if (_.isUndefined(params.passphrase)) {
Expand Down
73 changes: 73 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/ecdsa/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as openpgp from 'openpgp';
import { ec } from 'elliptic';

import { IBaseCoin } from '../../../baseCoin';
import baseTSSUtils from '../baseTSSUtils';
import { KeyShare } from './types';
import { BackupGpgKey } from '../baseTypes';
import { generateGPGKeyPair, getBitgoGpgPubKey, getTrustGpgPubKey } from '../../opengpgUtils';
import { BitGoBase } from '../../../bitgoBase';
import { IWallet } from '../../../wallet';

/** @inheritdoc */
export class BaseEcdsaUtils extends baseTSSUtils<KeyShare> {
// We do not have full support for 3-party verification (w/ external source) of key shares and signature shares. There is no 3rd party key service support with this release.
protected bitgoPublicGpgKey: openpgp.Key;

constructor(bitgo: BitGoBase, baseCoin: IBaseCoin, wallet?: IWallet) {
super(bitgo, baseCoin, wallet);
this.setBitgoGpgPubKey(bitgo);
}

private async setBitgoGpgPubKey(bitgo) {
this.bitgoPublicGpgKey = await getBitgoGpgPubKey(bitgo);
}

async getBitgoPublicGpgKey(): Promise<openpgp.Key> {
if (!this.bitgoPublicGpgKey) {
// retry getting bitgo's gpg key
await this.setBitgoGpgPubKey(this.bitgo);
if (!this.bitgoPublicGpgKey) {
throw new Error("Failed to get Bitgo's gpg key");
}
}

return this.bitgoPublicGpgKey;
}

/**
* Gets backup pub gpg key string
* if a third party provided then get from trust
* @param isThirdPartyBackup
*/
async getBackupGpgPubKey(isThirdPartyBackup = false): Promise<BackupGpgKey> {
return isThirdPartyBackup ? getTrustGpgPubKey(this.bitgo) : generateGPGKeyPair('secp256k1');
}

/**
* util function that checks that a commonKeychain is valid and can ultimately resolve to a valid public key
* @param commonKeychain - a user uploaded commonKeychain string
* @throws if the commonKeychain is invalid length or invalid format
*/

static validateCommonKeychainPublicKey(commonKeychain: string) {
const pub = BaseEcdsaUtils.getPublicKeyFromCommonKeychain(commonKeychain);
const secp256k1 = new ec('secp256k1');
const key = secp256k1.keyFromPublic(pub, 'hex');
return key.getPublic().encode('hex', false).slice(2);
}

/**
* Gets the common public key from commonKeychain.
*
* @param {String} commonKeychain common key chain between n parties
* @returns {string} encoded public key
*/
static getPublicKeyFromCommonKeychain(commonKeychain: string): string {
if (commonKeychain.length !== 130) {
throw new Error(`Invalid commonKeychain length, expected 130, got ${commonKeychain.length}`);
}
const commonPubHexStr = commonKeychain.slice(0, 66);
return commonPubHexStr;
}
}
Loading

0 comments on commit 4eaf7ff

Please sign in to comment.