diff --git a/src/api/enclaved/handlers/mpcV2Finalize.ts b/src/api/enclaved/handlers/ecdsaMPCv2Finalize.ts similarity index 98% rename from src/api/enclaved/handlers/mpcV2Finalize.ts rename to src/api/enclaved/handlers/ecdsaMPCv2Finalize.ts index c062fd12..27b3922c 100644 --- a/src/api/enclaved/handlers/mpcV2Finalize.ts +++ b/src/api/enclaved/handlers/ecdsaMPCv2Finalize.ts @@ -7,7 +7,7 @@ import { import { KmsClient } from '../../../kms/kmsClient'; import assert from 'assert'; -export async function mpcV2Finalize( +export async function ecdsaMPCv2Finalize( req: EnclavedApiSpecRouteRequest<'v1.mpcv2.finalize', 'post'>, ): Promise { const { source, encryptedData, encryptedDataKey, broadcastMessages, bitgoCommonKeychain } = diff --git a/src/api/enclaved/handlers/mpcV2Initialize.ts b/src/api/enclaved/handlers/ecdsaMPCv2Initialize.ts similarity index 97% rename from src/api/enclaved/handlers/mpcV2Initialize.ts rename to src/api/enclaved/handlers/ecdsaMPCv2Initialize.ts index 52535eba..6a053fcb 100644 --- a/src/api/enclaved/handlers/mpcV2Initialize.ts +++ b/src/api/enclaved/handlers/ecdsaMPCv2Initialize.ts @@ -8,7 +8,7 @@ import * as bitgoSdk from '@bitgo/sdk-core'; import logger from '../../../logger'; import { MPCv2PartiesEnum } from '@bitgo/sdk-core/dist/src/bitgo/utils/tss/ecdsa'; -export async function mpcV2Initialize( +export async function ecdsaMPCv2Initialize( req: EnclavedApiSpecRouteRequest<'v1.mpcv2.initialize', 'post'>, ): Promise { const { source } = req.decoded; diff --git a/src/api/enclaved/handlers/mpcV2Round.ts b/src/api/enclaved/handlers/ecdsaMPCv2Round.ts similarity index 99% rename from src/api/enclaved/handlers/mpcV2Round.ts rename to src/api/enclaved/handlers/ecdsaMPCv2Round.ts index 8a1467d1..54c2621b 100644 --- a/src/api/enclaved/handlers/mpcV2Round.ts +++ b/src/api/enclaved/handlers/ecdsaMPCv2Round.ts @@ -7,7 +7,7 @@ import { import { MPCv2PartiesEnum } from '@bitgo/sdk-core/dist/src/bitgo/utils/tss/ecdsa'; import { KmsClient } from '../../../kms/kmsClient'; -export async function mpcV2Round( +export async function ecdsaMPCv2Round( req: EnclavedApiSpecRouteRequest<'v1.mpcv2.round', 'post'>, ): Promise { const { source, encryptedData, encryptedDataKey, round, broadcastMessages, p2pMessages } = diff --git a/src/api/master/clients/enclavedExpressClient.ts b/src/api/master/clients/enclavedExpressClient.ts index d2dd4226..9937a1a6 100644 --- a/src/api/master/clients/enclavedExpressClient.ts +++ b/src/api/master/clients/enclavedExpressClient.ts @@ -508,7 +508,9 @@ export class EnclavedExpressClient { /** * Initialize MPCv2 key generation */ - async initMpcV2(params: { source: 'user' | 'backup' }): Promise { + async initEcdsaMpcV2KeyGenMpcV2(params: { + source: 'user' | 'backup'; + }): Promise { if (!this.coin) { throw new Error('Coin must be specified to initialize MPCv2 key generation'); } @@ -536,7 +538,7 @@ export class EnclavedExpressClient { /** * Execute a round in the MPCv2 protocol */ - async mpcV2Round(params: { + async roundEcdsaMPCv2KeyGen(params: { source: 'user' | 'backup'; encryptedData: string; encryptedDataKey: string; @@ -573,7 +575,7 @@ export class EnclavedExpressClient { /** * Finalize MPCv2 key generation */ - async mpcV2Finalize(params: { + async finalizeEcdsaMPCv2KeyGen(params: { source: 'user' | 'backup'; encryptedData: string; encryptedDataKey: string; diff --git a/src/api/master/handlers/ecdsa.ts b/src/api/master/handlers/ecdsa.ts index 524d4552..749c5bb8 100644 --- a/src/api/master/handlers/ecdsa.ts +++ b/src/api/master/handlers/ecdsa.ts @@ -1,11 +1,13 @@ import { + BaseCoin, BitGoBase, + commonTssMethods, + EcdsaMPCv2Utils, getTxRequest, - Wallet, IRequestTracer, - EcdsaMPCv2Utils, - commonTssMethods, RequestType, + SupplementGenerateWalletOptions, + Wallet, } from '@bitgo/sdk-core'; import { EnclavedExpressClient } from '../clients/enclavedExpressClient'; import logger from '../../../logger'; @@ -113,3 +115,286 @@ export async function handleEcdsaSigning( reqId, ); } + +interface OrchestrateEcdsaKeyGenParams { + bitgo: BitGoBase; + baseCoin: BaseCoin; + enclavedExpressClient: EnclavedExpressClient; + enterprise: string; + walletParams: SupplementGenerateWalletOptions; +} + +export async function orchestrateEcdsaKeyGen({ + bitgo, + baseCoin, + enclavedExpressClient, + enterprise, + walletParams, +}: OrchestrateEcdsaKeyGenParams) { + const constants = await bitgo.fetchConstants(); + if (!constants.mpc.bitgoMPCv2PublicKey) { + throw new Error('Unable to create MPCv2 keys - bitgoMPCv2PublicKey is missing in constants'); + } + const ecdsaUtils = new EcdsaMPCv2Utils(bitgo, baseCoin); + + // INITIALIZE ROUND: GENERATE ALL GPG KEYS AND RETRIEVE GPG PUBS FROM ALL PARTIES + const userInitResponse = await enclavedExpressClient.initEcdsaMpcV2KeyGenMpcV2({ + source: 'user', + }); + if ( + !userInitResponse.gpgPub || + !userInitResponse.encryptedData || + !userInitResponse.encryptedDataKey + ) { + throw new Error('Missing required fields in user init response'); + } + const backupInitResponse = await enclavedExpressClient.initEcdsaMpcV2KeyGenMpcV2({ + source: 'backup', + }); + if ( + !backupInitResponse.gpgPub || + !backupInitResponse.encryptedData || + !backupInitResponse.encryptedDataKey + ) { + throw new Error('Missing required fields in backup init response'); + } + + // ROUND 1 + const userRound1Promise = enclavedExpressClient.roundEcdsaMPCv2KeyGen({ + source: 'user', + encryptedData: userInitResponse.encryptedData, + encryptedDataKey: userInitResponse.encryptedDataKey, + round: 1, + bitgoGpgPub: constants.mpc.bitgoMPCv2PublicKey, + counterPartyGpgPub: backupInitResponse.gpgPub, + }); + const backupRound1Promise = enclavedExpressClient.roundEcdsaMPCv2KeyGen({ + source: 'backup', + encryptedData: backupInitResponse.encryptedData, + encryptedDataKey: backupInitResponse.encryptedDataKey, + round: 1, + bitgoGpgPub: constants.mpc.bitgoMPCv2PublicKey, + counterPartyGpgPub: userInitResponse.gpgPub, + }); + const [userRound1Response, backupRound1Response] = await Promise.all([ + userRound1Promise, + backupRound1Promise, + ]); + if (!userRound1Response.broadcastMessage) { + throw new Error('Missing broadcast message in user round 1 response'); + } + if (!backupRound1Response.broadcastMessage) { + throw new Error('Missing broadcast message in backup round 1 response'); + } + + // ROUND 1 & 2 BitGo + const round1And2BitGoResponse = await ecdsaUtils.sendKeyGenerationRound1( + enterprise, + userInitResponse.gpgPub, + backupInitResponse.gpgPub, + { + broadcastMessages: [ + userRound1Response.broadcastMessage, + backupRound1Response.broadcastMessage, + ], + p2pMessages: [], + }, + ); + const { sessionId, bitgoMsg1, bitgoToUserMsg2, bitgoToBackupMsg2 } = round1And2BitGoResponse; + + // ROUND 2 + const userRound2Promise = enclavedExpressClient.roundEcdsaMPCv2KeyGen({ + source: 'user', + encryptedData: userRound1Response.encryptedData, + encryptedDataKey: userRound1Response.encryptedDataKey, + round: 2, + broadcastMessages: { + bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg1), + counterParty: backupRound1Response.broadcastMessage, + }, + }); + const backupRound2Promise = enclavedExpressClient.roundEcdsaMPCv2KeyGen({ + source: 'backup', + encryptedData: backupRound1Response.encryptedData, + encryptedDataKey: backupRound1Response.encryptedDataKey, + round: 2, + broadcastMessages: { + bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg1), + counterParty: userRound1Response.broadcastMessage, + }, + }); + const [userRound2Response, backupRound2Response] = await Promise.all([ + userRound2Promise, + backupRound2Promise, + ]); + if (!userRound2Response.p2pMessages?.bitgo) { + throw new Error('Missing BitGo p2p message in user round 2 response'); + } + if (!backupRound2Response.p2pMessages?.bitgo) { + throw new Error('Missing BitGo p2p message in backup round 2 response'); + } + + // ROUND 3 + const userRound3Promise = enclavedExpressClient.roundEcdsaMPCv2KeyGen({ + source: 'user', + encryptedData: userRound2Response.encryptedData, + encryptedDataKey: userRound2Response.encryptedDataKey, + round: 3, + p2pMessages: { + bitgo: ecdsaUtils.formatP2PMessage(bitgoToUserMsg2), + counterParty: backupRound2Response.p2pMessages?.counterParty, + }, + }); + const backupRound3Promise = enclavedExpressClient.roundEcdsaMPCv2KeyGen({ + source: 'backup', + encryptedData: backupRound2Response.encryptedData, + encryptedDataKey: backupRound2Response.encryptedDataKey, + round: 3, + p2pMessages: { + bitgo: ecdsaUtils.formatP2PMessage(bitgoToBackupMsg2), + counterParty: userRound2Response.p2pMessages?.counterParty, + }, + }); + const round3BitGoPromise = ecdsaUtils.sendKeyGenerationRound2(enterprise, sessionId, { + p2pMessages: [ + userRound2Response.p2pMessages?.bitgo, + backupRound2Response.p2pMessages?.bitgo, + ].filter((msg) => msg !== undefined), + broadcastMessages: [], + }); + const [userRound3Response, backupRound3Response, round3BitGoResponse] = await Promise.all([ + userRound3Promise, + backupRound3Promise, + round3BitGoPromise, + ]); + const { + sessionId: sessionIdRound3, + bitgoToUserMsg3, + bitgoToBackupMsg3, + bitgoCommitment2: bitgoCommitment3, + } = round3BitGoResponse; + if (!userRound3Response.p2pMessages?.bitgo) { + throw new Error('Missing BitGo p2p message in user round 3 response'); + } + if (!backupRound3Response.p2pMessages?.bitgo) { + throw new Error('Missing BitGo p2p message in backup round 3 response'); + } + if (sessionId !== sessionIdRound3) { + throw new Error('Round 1 and 2 Session IDs do not match'); + } + + // ROUND 4 + const userRound4Promise = enclavedExpressClient.roundEcdsaMPCv2KeyGen({ + source: 'user', + encryptedData: userRound3Response.encryptedData, + encryptedDataKey: userRound3Response.encryptedDataKey, + round: 4, + p2pMessages: { + bitgo: ecdsaUtils.formatP2PMessage(bitgoToUserMsg3, bitgoCommitment3), + counterParty: backupRound3Response.p2pMessages?.counterParty, + }, + }); + const backupRound4Promise = enclavedExpressClient.roundEcdsaMPCv2KeyGen({ + source: 'backup', + encryptedData: backupRound3Response.encryptedData, + encryptedDataKey: backupRound3Response.encryptedDataKey, + round: 4, + p2pMessages: { + bitgo: ecdsaUtils.formatP2PMessage(bitgoToBackupMsg3, bitgoCommitment3), + counterParty: userRound3Response.p2pMessages?.counterParty, + }, + }); + const [userRound4Response, backupRound4Response] = await Promise.all([ + userRound4Promise, + backupRound4Promise, + ]); + if (!userRound4Response.broadcastMessage) { + throw new Error('Missing broadcast message in user round 4 response'); + } + if (!backupRound4Response.broadcastMessage) { + throw new Error('Missing broadcast message in backup round 4 response'); + } + + // FINALIZE + const round4BitGoResponse = await ecdsaUtils.sendKeyGenerationRound3(enterprise, sessionId, { + p2pMessages: [ + userRound3Response.p2pMessages?.bitgo, + backupRound3Response.p2pMessages?.bitgo, + ].filter((msg) => msg !== undefined), + broadcastMessages: [ + userRound4Response.broadcastMessage, + backupRound4Response.broadcastMessage, + ].filter((msg) => msg !== undefined), + }); + const { + sessionId: sessionIdRound4, + bitgoMsg4, + commonKeychain: bitgoCommonKeychain, + } = round4BitGoResponse; + const userFinalizePromise = enclavedExpressClient.finalizeEcdsaMPCv2KeyGen({ + source: 'user', + encryptedData: userRound4Response.encryptedData, + encryptedDataKey: userRound4Response.encryptedDataKey, + broadcastMessages: { + bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg4), + counterParty: backupRound4Response.broadcastMessage, + }, + bitgoCommonKeychain, + }); + const backupFinalizePromise = enclavedExpressClient.finalizeEcdsaMPCv2KeyGen({ + source: 'backup', + encryptedData: backupRound4Response.encryptedData, + encryptedDataKey: backupRound4Response.encryptedDataKey, + broadcastMessages: { + bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg4), + counterParty: userRound4Response.broadcastMessage, + }, + bitgoCommonKeychain, + }); + const [userFinalizeResponse, backupFinalizeResponse] = await Promise.all([ + userFinalizePromise, + backupFinalizePromise, + ]); + if (sessionId !== sessionIdRound4) { + throw new Error('Round 4 Session IDs do not match'); + } + if (!userFinalizeResponse.commonKeychain) { + throw new Error('Missing common keychain in user finalize response'); + } + if (!backupFinalizeResponse.commonKeychain) { + throw new Error('Missing common keychain in backup finalize response'); + } + if (userFinalizeResponse.commonKeychain !== backupFinalizeResponse.commonKeychain) { + throw new Error('User and backup common keychains do not match'); + } + if (userFinalizeResponse.commonKeychain !== bitgoCommonKeychain) { + throw new Error('User and BitGo common keychains do not match'); + } + + // CREATE KEYCHAINS + const userMpcKey = await baseCoin.keychains().add({ + commonKeychain: userFinalizeResponse.commonKeychain, + source: 'user', + type: 'tss', + isMPCv2: true, + }); + const backupMpcKey = await baseCoin.keychains().add({ + commonKeychain: backupFinalizeResponse.commonKeychain, + source: 'backup', + type: 'tss', + isMPCv2: true, + }); + const bitgoKeychain = await baseCoin.keychains().add({ + commonKeychain: bitgoCommonKeychain, + source: 'bitgo', + type: 'tss', + isMPCv2: true, + }); + walletParams.keys = [userMpcKey.id, backupMpcKey.id, bitgoKeychain.id]; + const keychains = { + userKeychain: userMpcKey, + backupKeychain: backupMpcKey, + bitgoKeychain, + }; + return { walletParams, keychains }; +} diff --git a/src/api/master/handlers/eddsa.ts b/src/api/master/handlers/eddsa.ts index e01d5ea1..d6118a3c 100644 --- a/src/api/master/handlers/eddsa.ts +++ b/src/api/master/handlers/eddsa.ts @@ -7,6 +7,8 @@ import { Wallet, IRequestTracer, EddsaUtils, + BaseCoin, + ApiKeyShare, } from '@bitgo/sdk-core'; import { EnclavedExpressClient } from '../clients/enclavedExpressClient'; import { exchangeEddsaCommitments } from '@bitgo/sdk-core/dist/src/bitgo/tss/common'; @@ -82,3 +84,117 @@ export async function handleEddsaSigning( logger.debug('Successfully completed signing!'); return await getTxRequest(bitgo, wallet.id(), txRequestId, reqId); } + +interface OrchestrateEddsaKeyGenParams { + bitgo: BitGoBase; + baseCoin: BaseCoin; + enclavedExpressClient: EnclavedExpressClient; + enterprise: string; + walletParams: any; +} + +export async function orchestrateEddsaKeyGen({ + bitgo, + baseCoin, + enclavedExpressClient, + enterprise, + walletParams, +}: OrchestrateEddsaKeyGenParams) { + const constants = await bitgo.fetchConstants(); + if (!constants.mpc.bitgoPublicKey) { + throw new Error('Unable to create MPC keys - bitgoPublicKey is missing in constants'); + } + // Initialize key generation for user and backup + const userInitResponse = await enclavedExpressClient.initMpcKeyGeneration({ + source: 'user', + bitgoGpgKey: constants.mpc.bitgoPublicKey, + }); + const backupInitResponse = await enclavedExpressClient.initMpcKeyGeneration({ + source: 'backup', + bitgoGpgKey: constants.mpc.bitgoPublicKey, + userGpgKey: userInitResponse.bitgoPayload.gpgKey, + }); + if (!backupInitResponse.counterPartyKeyShare) { + throw new Error('User key share is missing from initialization response'); + } + // Extract GPG keys based on payload type + const userGPGKey = + userInitResponse.bitgoPayload.from === 'user' + ? userInitResponse.bitgoPayload.gpgKey + : undefined; + const backupGPGKey = + backupInitResponse.bitgoPayload.from === 'backup' + ? backupInitResponse.bitgoPayload.gpgKey + : undefined; + if (!userGPGKey || !backupGPGKey) { + throw new Error('Missing required GPG keys from payloads'); + } + // Create BitGo keychain using the initialization responses + const bitgoKeychain = await baseCoin.keychains().add({ + keyType: 'tss', + source: 'bitgo', + keyShares: [userInitResponse.bitgoPayload, backupInitResponse.bitgoPayload], + enterprise: enterprise, + userGPGPublicKey: userGPGKey, + backupGPGPublicKey: backupGPGKey, + }); + // Finalize user and backup keychains + const userKeychainPromise = await enclavedExpressClient.finalizeMpcKeyGeneration({ + source: 'user', + coin: baseCoin.getFamily(), + encryptedDataKey: userInitResponse.encryptedDataKey, + encryptedData: userInitResponse.encryptedData, + bitGoKeychain: { + ...bitgoKeychain, + commonKeychain: bitgoKeychain.commonKeychain ?? '', + hsmType: bitgoKeychain.hsmType, + type: 'tss', + source: 'bitgo', + verifiedVssProof: true, + isBitGo: true, + isTrust: false, + keyShares: bitgoKeychain.keyShares as ApiKeyShare[], + }, + counterPartyGPGKey: backupGPGKey, + counterPartyKeyShare: backupInitResponse.counterPartyKeyShare, + }); + if (!userKeychainPromise.counterpartyKeyShare) { + throw new Error('Backup key share is missing from user keychain promise'); + } + const userMpcKey = await baseCoin.keychains().add({ + commonKeychain: userKeychainPromise.commonKeychain, + source: 'user', + type: 'tss', + }); + const backupKeychainPromise = await enclavedExpressClient.finalizeMpcKeyGeneration({ + source: 'backup', + coin: baseCoin.getFamily(), + encryptedDataKey: backupInitResponse.encryptedDataKey, + encryptedData: backupInitResponse.encryptedData, + bitGoKeychain: { + ...bitgoKeychain, + commonKeychain: bitgoKeychain.commonKeychain ?? '', + hsmType: bitgoKeychain.hsmType, + type: 'tss', + source: 'bitgo', + verifiedVssProof: true, + isBitGo: true, + isTrust: false, + keyShares: bitgoKeychain.keyShares as ApiKeyShare[], + }, + counterPartyGPGKey: userGPGKey, + counterPartyKeyShare: userKeychainPromise.counterpartyKeyShare, + }); + const backupMpcKey = await baseCoin.keychains().add({ + commonKeychain: backupKeychainPromise.commonKeychain, + source: 'backup', + type: 'tss', + }); + const keychains = { + userKeychain: userMpcKey, + backupKeychain: backupMpcKey, + bitgoKeychain, + }; + walletParams.keys = [userMpcKey.id, backupMpcKey.id, bitgoKeychain.id]; + return { walletParams, keychains }; +} diff --git a/src/api/master/handlers/generateWallet.ts b/src/api/master/handlers/generateWallet.ts index d2e59437..d0c93a66 100644 --- a/src/api/master/handlers/generateWallet.ts +++ b/src/api/master/handlers/generateWallet.ts @@ -1,6 +1,5 @@ import { AddKeychainOptions, - EcdsaMPCv2Utils, Keychain, KeychainsTriplet, NotImplementedError, @@ -12,10 +11,8 @@ import { } from '@bitgo/sdk-core'; import _ from 'lodash'; import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec'; -import { KeyShareType } from '../../../enclavedBitgoExpress/routers/enclavedApiSpec'; -import debug from 'debug'; - -const debugLogger = debug('bitgo:masterBitGoExpress:generateWallet'); +import { orchestrateEcdsaKeyGen } from './ecdsa'; +import { orchestrateEddsaKeyGen } from './eddsa'; /** * Request handler for generating a wallet on-premises. @@ -158,7 +155,6 @@ async function handleGenerateOnPremMpcWallet( const reqId = new RequestTracer(); const { label, enterprise } = req.decoded; - // Create wallet parameters with type assertion to allow 'tss' subtype const walletParams: SupplementGenerateWalletOptions = { label: label, m: 2, @@ -176,470 +172,42 @@ async function handleGenerateOnPremMpcWallet( walletParams.enterprise = enterprise; } - const constants = await bitgo.fetchConstants(); - if (!constants.mpc) { - throw new Error('Unable to create MPC keys - cannot fetch MPC constants'); + const algorithm = baseCoin.getMPCAlgorithm(); + let orchestrateResult; + switch (algorithm) { + case 'ecdsa': + orchestrateResult = await orchestrateEcdsaKeyGen({ + bitgo, + baseCoin, + enclavedExpressClient, + enterprise, + walletParams, + }); + break; + case 'eddsa': + orchestrateResult = await orchestrateEddsaKeyGen({ + bitgo, + baseCoin, + enclavedExpressClient, + walletParams, + enterprise, + }); + break; + default: + throw new Error(`Unsupported MPC algorithm: ${algorithm}`); } - // Check if this is an ECDSA wallet - const isEcdsa = baseCoin.getMPCAlgorithm() === 'ecdsa'; - - if (isEcdsa) { - if (!constants.mpc.bitgoMPCv2PublicKey) { - throw new Error('Unable to create MPCv2 keys - bitgoMPCv2PublicKey is missing in constants'); - } - const ecdsaUtils = new EcdsaMPCv2Utils(bitgo, baseCoin); - - // INITIALIZE ROUND: GENERATE ALL GPG KEYS AND RETRIEVE GPG PUBS FROM ALL PARTIES - // Initialize MPCv2 key generation - const userInitResponse = await enclavedExpressClient.initMpcV2({ - source: 'user', - }); - if ( - !userInitResponse.gpgPub || - !userInitResponse.encryptedData || - !userInitResponse.encryptedDataKey - ) { - throw new Error('Missing required fields in user init response'); - } - - const backupInitResponse = await enclavedExpressClient.initMpcV2({ - source: 'backup', - }); - if ( - !backupInitResponse.gpgPub || - !backupInitResponse.encryptedData || - !backupInitResponse.encryptedDataKey - ) { - throw new Error('Missing required fields in backup init response'); - } - - debugLogger('User MPCv2 key generation initialized:', userInitResponse); - debugLogger('Backup MPCv2 key generation initialized:', backupInitResponse); - - // IN ROUND n, EACH PARTY TAKES IN MSG (n-1) AND RETUNS MSG n - - // ROUND 1: PASS IN GPG PUBS AND NO MSGS, RETURNS FIRST BROADCAST MSG (MSG 1) - // bitgo's round 1 acts differently, the method requires the broadcast msgs and return p2p msg for round 2 as well - const userRound1Promise = enclavedExpressClient.mpcV2Round({ - source: 'user', - encryptedData: userInitResponse.encryptedData, - encryptedDataKey: userInitResponse.encryptedDataKey, - round: 1, - bitgoGpgPub: constants.mpc.bitgoMPCv2PublicKey, - counterPartyGpgPub: backupInitResponse.gpgPub, - }); - - const backupRound1Promise = enclavedExpressClient.mpcV2Round({ - source: 'backup', - encryptedData: backupInitResponse.encryptedData, - encryptedDataKey: backupInitResponse.encryptedDataKey, - round: 1, - bitgoGpgPub: constants.mpc.bitgoMPCv2PublicKey, - counterPartyGpgPub: userInitResponse.gpgPub, - }); - - const [userRound1Response, backupRound1Response] = await Promise.all([ - userRound1Promise, - backupRound1Promise, - ]); - if (!userRound1Response.broadcastMessage) { - throw new Error('Missing broadcast message in user round 1 response'); - } - if (!backupRound1Response.broadcastMessage) { - throw new Error('Missing broadcast message in backup round 1 response'); - } - - // this step cannot happen in parallel since it does round 1 and round 2 in one go - const round1And2BitGoResponse = await ecdsaUtils.sendKeyGenerationRound1( - enterprise, - userInitResponse.gpgPub, - backupInitResponse.gpgPub, - { - broadcastMessages: [ - userRound1Response.broadcastMessage, - backupRound1Response.broadcastMessage, - ], - p2pMessages: [], - }, - ); - // bitgo round 2 messages are generated here alongside the round 1 broadcast messages - const { sessionId, bitgoMsg1, bitgoToUserMsg2, bitgoToBackupMsg2 } = round1And2BitGoResponse; - - // ROUND 2: PASS IN FIRST BROADCAST MSG, RETURNS FIRST P2P MSG (MSG 2) - // bitgo's round 2 processing is DONE ALREADY in the previous step - const userRound2Promise = enclavedExpressClient.mpcV2Round({ - source: 'user', - encryptedData: userRound1Response.encryptedData, - encryptedDataKey: userRound1Response.encryptedDataKey, - round: 2, - broadcastMessages: { - bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg1), - counterParty: backupRound1Response.broadcastMessage, - }, - }); - - const backupRound2Promise = enclavedExpressClient.mpcV2Round({ - source: 'backup', - encryptedData: backupRound1Response.encryptedData, - encryptedDataKey: backupRound1Response.encryptedDataKey, - round: 2, - broadcastMessages: { - bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg1), - counterParty: userRound1Response.broadcastMessage, - }, - }); - - const [userRound2Response, backupRound2Response] = await Promise.all([ - userRound2Promise, - backupRound2Promise, - ]); - if (!userRound2Response.p2pMessages?.bitgo) { - throw new Error('Missing BitGo p2p message in user round 2 response'); - } - if (!backupRound2Response.p2pMessages?.bitgo) { - throw new Error('Missing BitGo p2p message in backup round 2 response'); - } - - // ROUND 3: PASS IN FIRST P2P MSG, RETURNS SECOND P2P MSG (MSG 3) - const userRound3Promise = enclavedExpressClient.mpcV2Round({ - source: 'user', - encryptedData: userRound2Response.encryptedData, - encryptedDataKey: userRound2Response.encryptedDataKey, - round: 3, - p2pMessages: { - bitgo: ecdsaUtils.formatP2PMessage(bitgoToUserMsg2), - counterParty: backupRound2Response.p2pMessages?.counterParty, - }, - }); - - const backupRound3Promise = enclavedExpressClient.mpcV2Round({ - source: 'backup', - encryptedData: backupRound2Response.encryptedData, - encryptedDataKey: backupRound2Response.encryptedDataKey, - round: 3, - p2pMessages: { - bitgo: ecdsaUtils.formatP2PMessage(bitgoToBackupMsg2), - counterParty: userRound2Response.p2pMessages?.counterParty, - }, - }); - - // the method is called round 2 but it actually does round 3 - const round3BitGoPromise = ecdsaUtils.sendKeyGenerationRound2(enterprise, sessionId, { - p2pMessages: [ - userRound2Response.p2pMessages?.bitgo, - backupRound2Response.p2pMessages?.bitgo, - ].filter((msg): msg is NonNullable => msg !== undefined), - broadcastMessages: [], - }); - - const [userRound3Response, backupRound3Response, round3BitGoResponse] = await Promise.all([ - userRound3Promise, - backupRound3Promise, - round3BitGoPromise, - ]); - - const { - sessionId: sessionIdRound3, - bitgoToUserMsg3, - bitgoToBackupMsg3, - bitgoCommitment2: bitgoCommitment3, // renamed for clarity - } = round3BitGoResponse; - - if (!userRound3Response.p2pMessages?.bitgo) { - throw new Error('Missing BitGo p2p message in user round 3 response'); - } - if (!backupRound3Response.p2pMessages?.bitgo) { - throw new Error('Missing BitGo p2p message in backup round 3 response'); - } - if (sessionId !== sessionIdRound3) { - throw new Error('Round 1 and 2 Session IDs do not match'); - } - - // ROUND 4: PASS IN SECOND P2P MSG, RETURNS SECOND BROADCAST MSG (MSG 4) - // bitgo's round 4 acts differently, it is delayed and will be done in one go with the finalize step - const userRound4Promise = enclavedExpressClient.mpcV2Round({ - source: 'user', - encryptedData: userRound3Response.encryptedData, - encryptedDataKey: userRound3Response.encryptedDataKey, - round: 4, - p2pMessages: { - bitgo: ecdsaUtils.formatP2PMessage(bitgoToUserMsg3, bitgoCommitment3), - counterParty: backupRound3Response.p2pMessages?.counterParty, - }, - }); - - const backupRound4Promise = enclavedExpressClient.mpcV2Round({ - source: 'backup', - encryptedData: backupRound3Response.encryptedData, - encryptedDataKey: backupRound3Response.encryptedDataKey, - round: 4, - p2pMessages: { - bitgo: ecdsaUtils.formatP2PMessage(bitgoToBackupMsg3, bitgoCommitment3), - counterParty: userRound3Response.p2pMessages?.counterParty, - }, - }); - - const [userRound4Response, backupRound4Response] = await Promise.all([ - userRound4Promise, - backupRound4Promise, - ]); - if (!userRound4Response.broadcastMessage) { - throw new Error('Missing broadcast message in user round 4 response'); - } - if (!backupRound4Response.broadcastMessage) { - throw new Error('Missing broadcast message in backup round 4 response'); - } - - debugLogger('Starting MPCv2 key finalization'); - - // FINALIZE ROUND: PASS IN BROADCAST MSG 4, RETURNS COMMON KEYCHAIN - // bitgo's round 4 is not done yet, so we will have to do it first - const round4BitGoResponse = await ecdsaUtils.sendKeyGenerationRound3(enterprise, sessionId, { - p2pMessages: [ - userRound3Response.p2pMessages?.bitgo, - backupRound3Response.p2pMessages?.bitgo, - ].filter((msg): msg is NonNullable => msg !== undefined), - broadcastMessages: [ - userRound4Response.broadcastMessage, - backupRound4Response.broadcastMessage, - ].filter((msg): msg is NonNullable => msg !== undefined), - }); - - const { - sessionId: sessionIdRound4, - bitgoMsg4, - commonKeychain: bitgoCommonKeychain, - } = round4BitGoResponse; - - const userFinalizePromise = enclavedExpressClient.mpcV2Finalize({ - source: 'user', - encryptedData: userRound4Response.encryptedData, - encryptedDataKey: userRound4Response.encryptedDataKey, - broadcastMessages: { - bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg4), - counterParty: backupRound4Response.broadcastMessage, - }, - bitgoCommonKeychain, - }); - - const backupFinalizePromise = enclavedExpressClient.mpcV2Finalize({ - source: 'backup', - encryptedData: backupRound4Response.encryptedData, - encryptedDataKey: backupRound4Response.encryptedDataKey, - broadcastMessages: { - bitgo: ecdsaUtils.formatBitgoBroadcastMessage(bitgoMsg4), - counterParty: userRound4Response.broadcastMessage, - }, - bitgoCommonKeychain, - }); - - const [userFinalizeResponse, backupFinalizeResponse] = await Promise.all([ - userFinalizePromise, - backupFinalizePromise, - ]); - - if (sessionId !== sessionIdRound4) { - throw new Error('Round 4 Session IDs do not match'); - } - if (!userFinalizeResponse.commonKeychain) { - throw new Error('Missing common keychain in user finalize response'); - } - if (!backupFinalizeResponse.commonKeychain) { - throw new Error('Missing common keychain in backup finalize response'); - } - - // Verify common keychains match - if (userFinalizeResponse.commonKeychain !== backupFinalizeResponse.commonKeychain) { - throw new Error('User and backup common keychains do not match'); - } - if (userFinalizeResponse.commonKeychain !== bitgoCommonKeychain) { - throw new Error('User and BitGo common keychains do not match'); - } - - debugLogger('MPCv2 key generation completed successfully'); - - // CLEANUP AND CREATE KEYCHAINS - // Create keychains - const userMpcKey = await baseCoin.keychains().add({ - commonKeychain: userFinalizeResponse.commonKeychain, - source: 'user', - type: 'tss', - isMPCv2: true, - }); - - const backupMpcKey = await baseCoin.keychains().add({ - commonKeychain: backupFinalizeResponse.commonKeychain, - source: 'backup', - type: 'tss', - isMPCv2: true, - }); - - const bitgoKeychain = await baseCoin.keychains().add({ - commonKeychain: bitgoCommonKeychain, - source: 'bitgo', - type: 'tss', - isMPCv2: true, - }); - - walletParams.keys = [userMpcKey.id, backupMpcKey.id, bitgoKeychain.id]; - - const keychains = { - userKeychain: userMpcKey, - backupKeychain: backupMpcKey, - bitgoKeychain, - }; - - const finalWalletParams = await baseCoin.supplementGenerateWallet(walletParams, keychains); - - bitgo.setRequestTracer(reqId); - const newWallet = await bitgo - .post(baseCoin.url('/wallet/add')) - .send(finalWalletParams) - .result(); - - const result: WalletWithKeychains = { - wallet: new Wallet(bitgo, baseCoin, newWallet), - userKeychain: userMpcKey, - backupKeychain: backupMpcKey, - bitgoKeychain: bitgoKeychain, - responseType: 'WalletWithKeychains', - }; - - return { ...result, wallet: result.wallet.toJSON() }; - } else { - if (!constants.mpc.bitgoPublicKey) { - throw new Error('Unable to create MPC keys - bitgoPublicKey is missing in constants'); - } - // Original EdDSA implementation - // Initialize key generation for user and backup - const userInitResponse = await enclavedExpressClient.initMpcKeyGeneration({ - source: 'user', - bitgoGpgKey: constants.mpc.bitgoPublicKey, - }); - - debugLogger('User MPC key generation initialized:', userInitResponse); - - const backupInitResponse = await enclavedExpressClient.initMpcKeyGeneration({ - source: 'backup', - bitgoGpgKey: constants.mpc.bitgoPublicKey, - userGpgKey: userInitResponse.bitgoPayload.gpgKey, - }); - if (!backupInitResponse.counterPartyKeyShare) { - throw new Error('User key share is missing from initialization response'); - } - - debugLogger('Backup MPC key generation initialized:', backupInitResponse); - - // Extract GPG keys based on payload type - const userGPGKey = - userInitResponse.bitgoPayload.from === 'user' - ? userInitResponse.bitgoPayload.gpgKey - : undefined; - - const backupGPGKey = - backupInitResponse.bitgoPayload.from === 'backup' - ? backupInitResponse.bitgoPayload.gpgKey - : undefined; - - if (!userGPGKey || !backupGPGKey) { - throw new Error('Missing required GPG keys from payloads'); - } - - // Create BitGo keychain using the initialization responses - const bitgoKeychain = await baseCoin.keychains().add({ - keyType: 'tss', - source: 'bitgo', - keyShares: [userInitResponse.bitgoPayload, backupInitResponse.bitgoPayload], - enterprise: req.decoded.enterprise, - userGPGPublicKey: userGPGKey, - backupGPGPublicKey: backupGPGKey, - reqId, - }); - - // Finalize user and backup keychains - const userKeychainPromise = await enclavedExpressClient.finalizeMpcKeyGeneration({ - source: 'user', - coin: req.params.coin, - encryptedDataKey: userInitResponse.encryptedDataKey, - encryptedData: userInitResponse.encryptedData, - bitGoKeychain: { - ...bitgoKeychain, - commonKeychain: bitgoKeychain.commonKeychain ?? '', - hsmType: bitgoKeychain.hsmType, - type: 'tss', - source: 'bitgo', - verifiedVssProof: true, - isBitGo: true, - isTrust: false, - keyShares: bitgoKeychain.keyShares as KeyShareType[], - }, - counterPartyGPGKey: backupGPGKey, - counterPartyKeyShare: backupInitResponse.counterPartyKeyShare, - }); - if (!userKeychainPromise.counterpartyKeyShare) { - throw new Error('Backup key share is missing from user keychain promise'); - } - - const userMpcKey = await baseCoin.keychains().add({ - commonKeychain: userKeychainPromise.commonKeychain, - source: 'user', - type: 'tss', - }); - - debugLogger('User key finalized', userMpcKey); - - const backupKeychainPromise = await enclavedExpressClient.finalizeMpcKeyGeneration({ - source: 'backup', - coin: req.params.coin, - encryptedDataKey: backupInitResponse.encryptedDataKey, - encryptedData: backupInitResponse.encryptedData, - bitGoKeychain: { - ...bitgoKeychain, - commonKeychain: bitgoKeychain.commonKeychain ?? '', - hsmType: bitgoKeychain.hsmType, - type: 'tss', - source: 'bitgo', - verifiedVssProof: true, - isBitGo: true, - isTrust: false, - keyShares: bitgoKeychain.keyShares as any, - }, - counterPartyGPGKey: userGPGKey, - counterPartyKeyShare: userKeychainPromise.counterpartyKeyShare as KeyShareType, - }); - - const backupMpcKey = await baseCoin.keychains().add({ - commonKeychain: backupKeychainPromise.commonKeychain, - source: 'backup', - type: 'tss', - }); - debugLogger('Backup keychain finalized:', backupMpcKey); - - walletParams.keys = [userMpcKey.id, backupMpcKey.id, bitgoKeychain.id]; - - const keychains = { - userKeychain: userMpcKey, - backupKeychain: backupMpcKey, - bitgoKeychain, - }; - - const finalWalletParams = await baseCoin.supplementGenerateWallet(walletParams, keychains); - - bitgo.setRequestTracer(reqId); - const newWallet = await bitgo - .post(baseCoin.url('/wallet/add')) - .send(finalWalletParams) - .result(); + const { keychains, walletParams: finalWalletParams } = orchestrateResult; + bitgo.setRequestTracer(reqId); + const newWallet = await bitgo.post(baseCoin.url('/wallet/add')).send(finalWalletParams).result(); - const result: WalletWithKeychains = { - wallet: new Wallet(bitgo, baseCoin, newWallet), - userKeychain: userMpcKey, - backupKeychain: backupMpcKey, - bitgoKeychain: bitgoKeychain, - responseType: 'WalletWithKeychains', - }; + const result: WalletWithKeychains = { + wallet: new Wallet(bitgo, baseCoin, newWallet), + userKeychain: keychains.userKeychain, + backupKeychain: keychains.backupKeychain, + bitgoKeychain: keychains.bitgoKeychain, + responseType: 'WalletWithKeychains', + }; - return { ...result, wallet: result.wallet.toJSON() }; - } + return { ...result, wallet: result.wallet.toJSON() }; } diff --git a/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts b/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts index a35c3574..9aceeb6c 100644 --- a/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts +++ b/src/enclavedBitgoExpress/routers/enclavedApiSpec.ts @@ -25,9 +25,9 @@ import { BitGoRequest } from '../../types/request'; import { eddsaInitialize } from '../../api/enclaved/mpcInitialize'; import { eddsaFinalize } from '../../api/enclaved/mpcFinalize'; import { DklsDkg, DklsTypes } from '@bitgo-beta/sdk-lib-mpc'; -import { mpcV2Initialize } from '../../api/enclaved/handlers/mpcV2Initialize'; -import { mpcV2Round } from '../../api/enclaved/handlers/mpcV2Round'; -import { mpcV2Finalize } from '../../api/enclaved/handlers/mpcV2Finalize'; +import { ecdsaMPCv2Initialize } from '../../api/enclaved/handlers/ecdsaMPCv2Initialize'; +import { ecdsaMPCv2Round } from '../../api/enclaved/handlers/ecdsaMPCv2Round'; +import { ecdsaMPCv2Finalize } from '../../api/enclaved/handlers/ecdsaMPCv2Finalize'; // Request type for /key/independent endpoint const IndependentKeyRequest = { @@ -508,7 +508,7 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter(async (req) => { const typedReq = req as EnclavedApiSpecRouteRequest<'v1.mpcv2.initialize', 'post'>; - const result = await mpcV2Initialize(typedReq); + const result = await ecdsaMPCv2Initialize(typedReq); return Response.ok(result); }), ]); @@ -516,7 +516,7 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter(async (req) => { const typedReq = req as EnclavedApiSpecRouteRequest<'v1.mpcv2.round', 'post'>; - const result = await mpcV2Round(typedReq); + const result = await ecdsaMPCv2Round(typedReq); return Response.ok(result); }), ]); @@ -524,7 +524,7 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter(async (req) => { const typedReq = req as EnclavedApiSpecRouteRequest<'v1.mpcv2.finalize', 'post'>; - const result = await mpcV2Finalize(typedReq); + const result = await ecdsaMPCv2Finalize(typedReq); return Response.ok(result); }), ]);