diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts index 08540a71c9..013a77f1c3 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts @@ -1214,5 +1214,31 @@ describe('TSS Utils:', async function () { return backupKeychain; } + describe('getPublicKeyFromCommonKeychain', function () { + // 32-byte ed25519 public key as hex (64 chars) — the format produced by DKG getSharePublicKey().toString('hex') + const mpcv2CommonKeychain = 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270'; + // MPCv1 appends a 32-byte chaincode after the public key + const chaincode = '9d91c2e6353202cf61f8f275158b3468e9a00f7872fc2fd310b72cd026e2e2f9'; + const mpcv1CommonKeychain = mpcv2CommonKeychain + chaincode; + + it('should decode to the same 32-byte public key for both MPCv1 (128 chars) and MPCv2 (64 chars)', function () { + mpcv1CommonKeychain.length.should.equal(128); + mpcv2CommonKeychain.length.should.equal(64); + + const v1Result = TssUtils.getPublicKeyFromCommonKeychain(mpcv1CommonKeychain); + const v2Result = TssUtils.getPublicKeyFromCommonKeychain(mpcv2CommonKeychain); + + v1Result.should.equal(v2Result); + v1Result.should.equal('ByMPeVxs7e8zGecu8n1M43Mq9qkxBSypNNwHeEu2N6vb'); + }); + + it('should throw for an invalid commonKeychain length', function () { + should.throws( + () => TssUtils.getPublicKeyFromCommonKeychain('abcd'), + /Invalid commonKeychain length, expected 64 \(MPCv2\) or 128 \(MPCv1\), got 4/ + ); + }); + }); + // #endregion Nock helpers }); diff --git a/modules/bitgo/test/v2/unit/wallets.ts b/modules/bitgo/test/v2/unit/wallets.ts index 206f937962..ae67d2d172 100644 --- a/modules/bitgo/test/v2/unit/wallets.ts +++ b/modules/bitgo/test/v2/unit/wallets.ts @@ -13,6 +13,7 @@ import { TssUtils, Wallets, ECDSAUtils, + EDDSAUtils, KeychainsTriplet, GenerateWalletOptions, Wallet, @@ -1372,6 +1373,131 @@ describe('V2 Wallets:', function () { }); }); + describe('Generate TSS EdDSA MPCv2 wallet:', async function () { + const sandbox = sinon.createSandbox(); + + beforeEach(function () { + const tssSettings: TssSettings = { + coinSettings: { + sol: { + walletCreationSettings: { + multiSigTypeVersion: 'MPCv2', + }, + }, + }, + }; + nock('https://bitgo.fakeurl').get(`/api/v2/tss/settings`).times(2).reply(200, tssSettings); + }); + + afterEach(function () { + nock.cleanAll(); + sandbox.verifyAndRestore(); + }); + + it('should create a new tsol TSS EdDSA MPCv2 hot wallet', async function () { + const testCoin = bitgo.coin('tsol'); + 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', + }, + }; + const stubCreateKeychains = sandbox + .stub(EDDSAUtils.EddsaMPCv2Utils.prototype, 'createKeychains') + .resolves(stubbedKeychainsTriplet); + + const walletNock = nock('https://bitgo.fakeurl').post('/api/v2/tsol/wallet/add').reply(200); + + const wallets = new Wallets(bitgo, testCoin); + + const params = { + label: 'tss eddsa mpcv2 wallet', + passphrase: 'tss password', + multisigType: 'tss' as const, + enterprise: 'enterprise', + passcodeEncryptionCode: 'originalPasscodeEncryptionCode', + }; + + const response = await wallets.generateWallet(params); + + walletNock.isDone().should.be.true(); + stubCreateKeychains.calledOnce.should.be.true(); + + assert.ok(response.encryptedWalletPassphrase); + assert.ok(response.wallet); + assert.equal( + bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }), + params.passphrase + ); + }); + + it('should fall back to EddsaUtils when tss/settings does not return MPCv2 for tsol', async function () { + nock.cleanAll(); + nock('https://bitgo.fakeurl') + .get('/api/v2/tss/settings') + .reply(200, { coinSettings: { sol: { walletCreationSettings: {} } } }); + + const testCoin = bitgo.coin('tsol'); + 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', + }, + }; + const stubEddsaUtils = sandbox + .stub(EDDSAUtils.default.prototype, 'createKeychains') + .resolves(stubbedKeychainsTriplet); + + const walletNock = nock('https://bitgo.fakeurl').post('/api/v2/tsol/wallet/add').reply(200); + + const wallets = new Wallets(bitgo, testCoin); + + const params = { + label: 'tss eddsa wallet', + passphrase: 'tss password', + multisigType: 'tss' as const, + enterprise: 'enterprise', + passcodeEncryptionCode: 'originalPasscodeEncryptionCode', + }; + + const response = await wallets.generateWallet(params); + + walletNock.isDone().should.be.true(); + stubEddsaUtils.calledOnce.should.be.true(); + + assert.ok(response.encryptedWalletPassphrase); + assert.ok(response.wallet); + }); + }); + describe('Sharing', () => { describe('Wallet share where keychainOverrideRequired is set true', () => { const sandbox = sinon.createSandbox(); diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index 936d4edd3d..9f127d619f 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -7,7 +7,7 @@ import { IMarkets } from '../market'; import { IPendingApprovals } from '../pendingApproval'; import { InitiateRecoveryOptions } from '../recovery'; import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa'; -import EddsaUtils, { PrebuildTransactionWithIntentOptions, TxRequest } from '../utils/tss/eddsa'; +import EddsaUtils, { EddsaMPCv2Utils, PrebuildTransactionWithIntentOptions, TxRequest } from '../utils/tss/eddsa'; import { CreateAddressFormat, CustomSigningFunction, IWallet, IWallets, Memo, Wallet, WalletData } from '../wallet'; import { TokenEnablement } from '@bitgo/public-types'; @@ -283,7 +283,7 @@ export interface ExtraPrebuildParamsOptions { export interface PresignTransactionOptions { txPrebuild?: TransactionPrebuild; walletData: WalletData; - tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | undefined; + tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | undefined; [index: string]: unknown; } diff --git a/modules/sdk-core/src/bitgo/keychain/keychains.ts b/modules/sdk-core/src/bitgo/keychain/keychains.ts index 93da7a7dc6..e299c04225 100644 --- a/modules/sdk-core/src/bitgo/keychain/keychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/keychains.ts @@ -305,28 +305,21 @@ export class Keychains implements IKeychains { * @return {Promise} newly created User, Backup, and BitGo keys */ async createMpc(params: CreateMpcOptions): Promise { - let MpcUtils; - let multisigTypeVersion: 'MPCv2' | undefined = undefined; - if (params.multisigType === 'tss' && this.baseCoin.getMPCAlgorithm() === 'ecdsa') { - const tssSettings: TssSettings = await this.bitgo - .get(this.bitgo.microservicesUrl('/api/v2/tss/settings')) - .result(); - multisigTypeVersion = - tssSettings.coinSettings[this.baseCoin.getFamily()]?.walletCreationSettings?.multiSigTypeVersion; + if (params.multisigType !== 'tss') { + throw new Error('Unsupported multi-sig type'); } - switch (params.multisigType) { - case 'tss': - MpcUtils = - this.baseCoin.getMPCAlgorithm() === 'eddsa' - ? EDDSAUtils.default - : multisigTypeVersion === 'MPCv2' - ? ECDSAUtils.EcdsaMPCv2Utils - : ECDSAUtils.EcdsaUtils; - break; - default: - throw new Error('Unsupported multi-sig type'); + const tssSettings: TssSettings = await this.bitgo.get(this.bitgo.microservicesUrl('/api/v2/tss/settings')).result(); + const multisigTypeVersion = + tssSettings.coinSettings[this.baseCoin.getFamily()]?.walletCreationSettings?.multiSigTypeVersion; + + let MpcUtils; + if (this.baseCoin.getMPCAlgorithm() === 'eddsa') { + MpcUtils = multisigTypeVersion === 'MPCv2' ? EDDSAUtils.EddsaMPCv2Utils : EDDSAUtils.default; + } else { + MpcUtils = multisigTypeVersion === 'MPCv2' ? ECDSAUtils.EcdsaMPCv2Utils : ECDSAUtils.EcdsaUtils; } + const mpcUtils = new MpcUtils(this.bitgo, this.baseCoin); return await mpcUtils.createKeychains({ passphrase: params.passphrase, diff --git a/modules/sdk-core/src/bitgo/pendingApproval/pendingApproval.ts b/modules/sdk-core/src/bitgo/pendingApproval/pendingApproval.ts index ebf7e898ff..5ca48252d4 100644 --- a/modules/sdk-core/src/bitgo/pendingApproval/pendingApproval.ts +++ b/modules/sdk-core/src/bitgo/pendingApproval/pendingApproval.ts @@ -19,7 +19,7 @@ import { IWallet } from '../wallet'; import { BuildParams } from '../wallet/BuildParams'; import { IRequestTracer } from '../../api'; import BaseTssUtils from '../utils/tss/baseTSSUtils'; -import EddsaUtils from '../utils/tss/eddsa'; +import EddsaUtils, { EddsaMPCv2Utils } from '../utils/tss/eddsa'; import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa'; import { KeyShare as EcdsaKeyShare } from '../utils/tss/ecdsa/types'; import { KeyShare as EddsaKeyShare } from '../utils/tss/eddsa/types'; @@ -57,7 +57,11 @@ export class PendingApproval implements IPendingApproval { this.tssUtils = new EcdsaUtils(this.bitgo, this.baseCoin, wallet); } } else { - this.tssUtils = new EddsaUtils(this.bitgo, this.baseCoin, wallet); + if (this.wallet?.multisigTypeVersion() === 'MPCv2') { + this.tssUtils = new EddsaMPCv2Utils(this.bitgo, this.baseCoin, wallet); + } else { + this.tssUtils = new EddsaUtils(this.bitgo, this.baseCoin, wallet); + } } } diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/base.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/base.ts index 5f7da3a20c..5f4561a216 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/base.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/base.ts @@ -1,3 +1,4 @@ +import * as bs58 from 'bs58'; import { IBaseCoin } from '../../../baseCoin'; import baseTSSUtils from '../baseTSSUtils'; import { KeyShare } from './types'; @@ -9,4 +10,25 @@ export class BaseEddsaUtils extends baseTSSUtils { super(bitgo, baseCoin, wallet); this.setBitgoGpgPubKey(bitgo); } + + /** + * Get the commonPub portion of an EdDSA commonKeychain. + * + * MPCv1 keychains are 128 hex chars (32-byte public key + 32-byte chaincode). + * MPCv2 keychains are 64 hex chars (32-byte public key only — no chaincode). + * + * @param {string} commonKeychain + * @returns {string} base58-encoded public key + */ + static getPublicKeyFromCommonKeychain(commonKeychain: string): string { + if (commonKeychain.length !== 64 && commonKeychain.length !== 128) { + throw new Error( + `Invalid commonKeychain length, expected 64 (MPCv2) or 128 (MPCv1), got ${commonKeychain.length}` + ); + } + // For MPCv1 (128 chars): the first 64 hex chars are the 32-byte public key. + // For MPCv2 (64 chars): the entire string is the 32-byte public key. + const pubHex = commonKeychain.slice(0, 64); + return bs58.encode(Buffer.from(pubHex, 'hex')); + } } diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts index 41168c28dd..de4790dfd5 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -2,7 +2,6 @@ * @prettier */ import assert from 'assert'; -import * as bs58 from 'bs58'; import * as openpgp from 'openpgp'; import Eddsa, { GShare, SignShare } from '../../../../account-lib/mpc/tss'; import { AddKeychainOptions, CreateBackupOptions, Keychain } from '../../../keychain'; @@ -36,6 +35,7 @@ import { } from '../baseTypes'; import { CreateEddsaBitGoKeychainParams, CreateEddsaKeychainParams, KeyShare, YShare } from './types'; import baseTSSUtils from '../baseTSSUtils'; +import { BaseEddsaUtils } from './base'; import { KeychainsTriplet } from '../../../baseCoin'; import { exchangeEddsaCommitments } from '../../../tss/common'; import { Ed25519Bip32HdTree } from '@bitgo/sdk-lib-mpc'; @@ -713,11 +713,7 @@ export class EddsaUtils extends baseTSSUtils { * @returns {string} */ static getPublicKeyFromCommonKeychain(commonKeychain: string): string { - if (commonKeychain.length !== 128) { - throw new Error(`Invalid commonKeychain length, expected 128, got ${commonKeychain.length}`); - } - const commonPubHexStr = commonKeychain.slice(0, 64); - return bs58.encode(Buffer.from(commonPubHexStr, 'hex')); + return BaseEddsaUtils.getPublicKeyFromCommonKeychain(commonKeychain); } createUserToBitgoCommitmentShare(commitment: string): CommitmentShareRecord { diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 69a95271c2..97a124e777 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -50,7 +50,7 @@ import { } from '../utils'; import { postWithCodec } from '../utils/postWithCodec'; import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa'; -import EddsaUtils from '../utils/tss/eddsa'; +import EddsaUtils, { EddsaMPCv2Utils } from '../utils/tss/eddsa'; import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest'; import { buildParamKeys, BuildParams } from './BuildParams'; import { @@ -155,7 +155,7 @@ export class Wallet implements IWallet { public readonly bitgo: BitGoBase; public readonly baseCoin: IBaseCoin; public _wallet: WalletData; - private readonly tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | undefined; + private readonly tssUtils: EcdsaUtils | EcdsaMPCv2Utils | EddsaUtils | EddsaMPCv2Utils | undefined; private readonly _permissions?: string[]; constructor(bitgo: BitGoBase, baseCoin: IBaseCoin, walletData: any) { @@ -177,7 +177,11 @@ export class Wallet implements IWallet { } break; case 'eddsa': - this.tssUtils = new EddsaUtils(bitgo, baseCoin, this); + if (walletData.multisigTypeVersion === 'MPCv2') { + this.tssUtils = new EddsaMPCv2Utils(bitgo, baseCoin, this); + } else { + this.tssUtils = new EddsaUtils(bitgo, baseCoin, this); + } break; default: this.tssUtils = undefined;