From bb686a78ad85221e9ab3fb6b62838402ba007646 Mon Sep 17 00:00:00 2001 From: Marzooqa Naeema Kather Date: Wed, 6 May 2026 01:52:15 +0530 Subject: [PATCH] feat(sdk-core): add EdDSA MPCv2 full 3-round signing orchestration - Add signTxRequest, signTxRequestForMessage, and signRequestBase to EddsaMPCv2Utils implementing the full online DSG protocol: WASM round 0, API rounds 1-3, and sendTxRequest - Verify PGP signatures on BitGo round1Output and round2Output - Use pickBitgoPubGpgKeyForSigning with isEddsaMpcv2=true for ed25519 key - Use decodeWithCodec for type-safe parsing of signature share responses - Add comprehensive signTxRequest unit tests covering: - tx signing (all 3 rounds + send) - message signing (all 3 rounds + send) - 429 rate-limit retry handling (up to 3 retries) - rejection after 4+ consecutive 429 errors - rejection on wrong round1Output type Co-Authored-By: Claude Sonnet 4.6 TICKET: WCI-154 --- .../tssUtils/eddsaMPCv2/signTxRequest.ts | 455 ++++++++++++++++++ .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 206 +++++++- 2 files changed, 660 insertions(+), 1 deletion(-) create mode 100644 modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts new file mode 100644 index 0000000000..e95d63a38f --- /dev/null +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts @@ -0,0 +1,455 @@ +import { + BaseCoin, + BitgoGPGPublicKey, + common, + ECDSAUtils, + EDDSAUtils, + RequestTracer, + RequestType, + SignatureShareRecord, + SignatureShareType, + TxRequest, + Wallet, +} from '@bitgo/sdk-core'; +import { EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc'; +import * as openpgp from 'openpgp'; +import nock = require('nock'); +import { TestableBG, TestBitGo } from '@bitgo/sdk-test'; +import { + EddsaMPCv2SignatureShareRound1Input, + EddsaMPCv2SignatureShareRound1Output, + EddsaMPCv2SignatureShareRound2Input, + EddsaMPCv2SignatureShareRound2Output, + EddsaMPCv2SignatureShareRound3Input, +} from '@bitgo/public-types'; +import { BitGo } from '../../../../../../src'; + +const MPCv2PartiesEnum = ECDSAUtils.MPCv2PartiesEnum; + +interface SignatureShareApiBody { + signatureShares: SignatureShareRecord[]; + signerGpgPublicKey: string; +} + +describe('signTxRequest:', function () { + let tssUtils: EDDSAUtils.EddsaMPCv2Utils; + let wallet: Wallet; + let bitgo: TestableBG & BitGo; + let baseCoin: BaseCoin; + let bitgoGpgKey: openpgp.SerializedKeyPair & { revocationCertificate: string }; + let bitgoPrvKeyObj: openpgp.PrivateKey; + const coinName = 'sol'; + + const reqId = new RequestTracer(); + const txRequestId = 'randomTxReqId'; + const signableHex = + '02010206c2d5b5f4fb9a9bcd8a2f303e4d06f78d8ded300713f456da2abff0b3ea0185aa051a34bc8acd438763976f96876115050f73828553566d111d7ac8bffebf587c4f5f5987bfe26aa66013efd96d36360f2b4336c91f993259fb56051305614d42f2ea13f8ff9d7958dbf269c6e36bfdf5cb5c43de4b4e1d3efb7dab3d5d028604000000000000000000000000000000000000000000000000000000000000000006a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea94000003a621f6d1cc4b8fb2a739aa08e4034da0fc588ece3bd857630de30f7edde45dd0204030205010404000000040200030c02000000f0a29a3b00000000'; + const serializedTxHex = `02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003bc9df0b397bec2ed3b6444a8c33f38267cc08b5fb2a7d37e26b6c487e26d15b7c07830eb78e26a88db5de4aa6986a327f09aed8c01533e5b972748ddf60b80f${signableHex}`; + const messageRaw = 'TOO MANY SECRETS'; + const messageEncoded = Buffer.from(messageRaw).toString('hex'); + const txParams = { + recipients: [ + { + address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs', + amount: '999990000', + }, + ], + }; + const txRequest: TxRequest = { + txRequestId, + enterpriseId: '4517abfb-f567-4b7a-9f91-407509d29403', + transactions: [ + { + unsignedTx: { + serializedTxHex, + signableHex, + derivationPath: 'm/0', // Needs this when key derivation is supported + }, + state: 'pendingSignature', + signatureShares: [], + }, + ], + unsignedTxs: [], + date: new Date().toISOString(), + intent: { intentType: 'payment' }, + latest: true, + state: 'pendingUserSignature', + walletType: 'hot', + walletId: 'walletId', + policiesChecked: true, + version: 1, + userId: 'userId', + apiVersion: 'full', + }; + + const txRequestForMessageSigning: TxRequest = { + txRequestId, + enterpriseId: '4517abfb-f567-4b7a-9f91-407509d29403', + messages: [ + { + messageRaw, + messageEncoded, + derivationPath: 'm/0', + state: 'pendingSignature', + signatureShares: [], + }, + ], + unsignedTxs: [], + date: new Date().toISOString(), + intent: { intentType: 'payment' }, + latest: true, + state: 'pendingUserSignature', + walletType: 'hot', + walletId: 'walletId', + policiesChecked: true, + version: 1, + userId: 'userId', + apiVersion: 'full', + }; + + let userKeyShare: Buffer; + let bitgoKeyShare: Buffer; + + before(async () => { + bitgo = TestBitGo.decorate(BitGo, { env: 'mock' }); + bitgo.initializeTestVars(); + const bgUrl = common.Environments[bitgo.getEnv()].uri; + bitgoGpgKey = await openpgp.generateKey({ + userIDs: [{ name: 'bitgo', email: 'bitgo@test.com' }], + curve: 'ed25519', + format: 'armored', + }); + bitgoPrvKeyObj = await openpgp.readPrivateKey({ armoredKey: bitgoGpgKey.privateKey }); + const constants = { + mpc: { + bitgoPublicKey: bitgoGpgKey.publicKey, + bitgoEddsaMpcv2PublicKey: bitgoGpgKey.publicKey, + }, + }; + nock(bgUrl).get('/api/v1/client/constants').times(20).reply(200, { ttl: 3600, constants }); + + const [userDkg, , bitgoDkg] = await MPSUtil.generateEdDsaDKGKeyShares(); + userKeyShare = userDkg.getKeyShare(); + bitgoKeyShare = bitgoDkg.getKeyShare(); + + baseCoin = bitgo.coin(coinName); + + const walletData = { + id: txRequest.walletId, + enterprise: txRequest.enterpriseId, + coin: coinName, + coinSpecific: { + rootAddress: 'E7Z6pFfUhjx2dFjdB9Ws2KnKepXoq62TeF5uaCVSvqQV', + }, + multisigType: 'tss', + multisigTypeVersion: 'MPCv2', + }; + wallet = new Wallet(bitgo, baseCoin, walletData); + tssUtils = new EDDSAUtils.EddsaMPCv2Utils(bitgo, baseCoin, wallet); + }); + + beforeEach(async function () { + await nockGetBitgoPublicKeyBasedOnFeatureFlags(coinName, txRequest.enterpriseId!, bitgoGpgKey); + }); + + after(function () { + nock.cleanAll(); + }); + + afterEach(function () { + nock.cleanAll(); + }); + + it('successfully signs a txRequest with user key for an mps hot wallet with WP', async function () { + const nockPromises = await getNockPromisesForEddsaSigning(txRequest); + await Promise.all(nockPromises); + + const userPrvBase64 = Buffer.from(userKeyShare).toString('base64'); + await tssUtils.signTxRequest({ + txRequest, + prv: userPrvBase64, + reqId, + txParams, + }); + nockPromises[0].isDone().should.be.true(); + nockPromises[1].isDone().should.be.true(); + nockPromises[2].isDone().should.be.true(); + nockPromises[3].isDone().should.be.true(); + }); + + it('successfully signs a txRequest with a message for an mps hot wallet with WP', async function () { + const nockPromises = await getNockPromisesForEddsaSigning(txRequestForMessageSigning, RequestType.message); + await Promise.all(nockPromises); + + const userPrvBase64 = Buffer.from(userKeyShare).toString('base64'); + await tssUtils.signTxRequestForMessage({ + txRequest: txRequestForMessageSigning, + prv: userPrvBase64, + reqId, + messageRaw: txRequestForMessageSigning.messages![0].messageRaw, + bufferToSign: Buffer.from(messageEncoded, 'hex'), + }); + nockPromises[0].isDone().should.be.true(); + nockPromises[1].isDone().should.be.true(); + nockPromises[2].isDone().should.be.true(); + nockPromises[3].isDone().should.be.true(); + }); + + it('should throw if round 1 response has wrong type', async function () { + nock('https://bitgo.fakeurl') + .post(`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}/transactions/0/sign`) + .reply(200, { + txRequestId, + transactions: [ + { + signatureShares: [ + { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify({ type: 'round2Output', data: {} }), + }, + ], + }, + ], + }); + + const userPrvBase64 = Buffer.from(userKeyShare).toString('base64'); + await tssUtils + .signTxRequest({ txRequest, prv: userPrvBase64, reqId, txParams }) + .should.be.rejectedWith(/Unexpected signature share response/); + }); + + it('successfully signs a txRequest for an mps hot wallet after receiving multiple 429 errors', async function () { + const nockPromises = await getNockPromisesForEddsaSigning(txRequest, RequestType.tx, 3); + await Promise.all(nockPromises); + + const userPrvBase64 = Buffer.from(userKeyShare).toString('base64'); + await tssUtils.signTxRequest({ + txRequest, + prv: userPrvBase64, + reqId, + txParams, + }); + nockPromises[0].isDone().should.be.true(); + nockPromises[1].isDone().should.be.true(); + nockPromises[2].isDone().should.be.true(); + nockPromises[3].isDone().should.be.true(); + }); + + it('fails to signs a txRequest for an mps hot wallet after receiving over 3 429 errors', async function () { + const nockPromises = await getNockPromisesForEddsaSigning(txRequest, RequestType.tx, 4); + await Promise.all(nockPromises); + + const userPrvBase64 = Buffer.from(userKeyShare).toString('base64'); + await tssUtils + .signTxRequest({ + txRequest, + prv: userPrvBase64, + reqId, + txParams, + }) + .should.be.rejectedWith('Too many requests, slow down!'); + nockPromises[0].isDone().should.be.true(); + nockPromises[1].isDone().should.be.false(); + nockPromises[2].isDone().should.be.false(); + nockPromises[3].isDone().should.be.false(); + }); + + async function getNockPromisesForEddsaSigning( + txRequest: TxRequest, + requestType: RequestType = RequestType.tx, + rateLimitErrorCount = 0 + ): Promise { + const txOrMessageToSign = + requestType === RequestType.message + ? txRequest.messages![0].messageEncoded! + : txRequest.transactions![0].unsignedTx.signableHex; + const messageBuffer = Buffer.from(txOrMessageToSign, 'hex'); + const bitgoSession = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO); + bitgoSession.initDsg( + bitgoKeyShare, + messageBuffer, + txRequest.transactions?.[0].unsignedTx.derivationPath || 'm/0', + MPCv2PartiesEnum.USER + ); + const bitgoMsg1 = bitgoSession.getFirstMessage(); + let bitgoMsg2: MPSTypes.DeserializedMessage | undefined; + + return [ + await nockTxRequestResponseSignatureShareRoundOne( + bitgoSession, + txRequest, + bitgoMsg1, + bitgoPrvKeyObj, + (msg) => { + bitgoMsg2 = msg; + }, + requestType + ), + await nockTxRequestResponseSignatureShareRoundTwo( + txRequest, + () => bitgoMsg2!, + bitgoPrvKeyObj, + requestType, + rateLimitErrorCount + ), + await nockTxRequestResponseSignatureShareRoundThree(txRequest, requestType), + await nockSendTxRequest(txRequest, requestType), + ]; + } +}); + +async function nockGetBitgoPublicKeyBasedOnFeatureFlags( + coin: string, + enterpriseId: string, + bitgoGpgKeyPair: openpgp.SerializedKeyPair +): Promise { + const bitgoGPGPublicKeyResponse: BitgoGPGPublicKey = { + name: 'irrelevant', + publicKey: bitgoGpgKeyPair.publicKey, + mpcv2PublicKey: bitgoGpgKeyPair.publicKey, + eddsaMpcv2PublicKey: bitgoGpgKeyPair.publicKey, + enterpriseId, + }; + nock('https://bitgo.fakeurl') + .get(`/api/v2/${coin}/tss/pubkey`) + .times(4) + .query({ enterpriseId }) + .reply(200, bitgoGPGPublicKeyResponse); + return bitgoGPGPublicKeyResponse; +} + +async function nockTxRequestResponseSignatureShareRoundOne( + bitgoSession: EddsaMPSDsg.DSG, + txRequest: TxRequest, + bitgoMsg1: MPSTypes.DeserializedMessage, + bitgoGpgPrivKey: openpgp.PrivateKey, + saveBitgoMsg2: (msg: MPSTypes.DeserializedMessage) => void, + requestType: RequestType = RequestType.tx +): Promise { + const route = requestType === RequestType.message ? '/messages/0' : '/transactions/0'; + return nock('https://bitgo.fakeurl') + .post( + `/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}${route}/sign`, + (body) => + (JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound1Input).type === 'round1Input' + ) + .reply(200, async (_uri: string, body: SignatureShareApiBody) => { + const parsedShare = JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound1Input; + const userMsg1Bytes = Buffer.from(parsedShare.data.msg1.message, 'base64'); + const userDeserializedMsg1: MPSTypes.DeserializedMessage = { + from: MPCv2PartiesEnum.USER, + payload: new Uint8Array(userMsg1Bytes), + }; + const [bitgoMsg2] = bitgoSession.handleIncomingMessages([bitgoMsg1, userDeserializedMsg1]); + saveBitgoMsg2(bitgoMsg2); + + const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); + const round1Output: EddsaMPCv2SignatureShareRound1Output = { + type: 'round1Output', + data: { msg1: bitgoSignedMsg1 }, + }; + const resource = requestType === RequestType.message ? 'messages' : 'transactions'; + return { + txRequestId: txRequest.txRequestId, + [resource]: [ + { + signatureShares: [ + { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify(round1Output), + }, + ], + }, + ], + }; + }); +} + +async function nockTxRequestResponseSignatureShareRoundTwo( + txRequest: TxRequest, + getBitgoMsg2: () => MPSTypes.DeserializedMessage, + bitgoGpgPrivKey: openpgp.PrivateKey, + requestType: RequestType = RequestType.tx, + rateLimitErrorCount = 0 +): Promise { + const route = requestType === RequestType.message ? '/messages/0' : '/transactions/0'; + const scope = nock('https://bitgo.fakeurl'); + + if (rateLimitErrorCount > 0) { + scope + .post( + `/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}${route}/sign`, + (body) => + (JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound2Input).type === 'round2Input' + ) + .times(rateLimitErrorCount) + .reply(429, { + error: 'Too many requests, slow down!', + name: 'TooManyRequests', + requestId: 'cm5qx01lh0013b2ek2sxl4w00', + context: {}, + }); + } + + return scope + .post( + `/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}${route}/sign`, + (body) => + (JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound2Input).type === 'round2Input' + ) + .reply(200, async () => { + const bitgoMsg2 = getBitgoMsg2(); + const bitgoSignedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg2.payload), bitgoGpgPrivKey); + const round2Output: EddsaMPCv2SignatureShareRound2Output = { + type: 'round2Output', + data: { msg2: bitgoSignedMsg2 }, + }; + const resource = requestType === RequestType.message ? 'messages' : 'transactions'; + return { + txRequestId: txRequest.txRequestId, + [resource]: [ + { + signatureShares: [ + { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + share: 'placeholder', + }, + { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify(round2Output), + }, + ], + }, + ], + }; + }); +} + +async function nockTxRequestResponseSignatureShareRoundThree( + txRequest: TxRequest, + requestType: RequestType = RequestType.tx +): Promise { + const route = requestType === RequestType.message ? '/messages/0' : '/transactions/0'; + return nock('https://bitgo.fakeurl') + .post( + `/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}${route}/sign`, + (body: SignatureShareApiBody) => + (JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound3Input).type === 'round3Input' + ) + .reply(200, { + txRequestId: txRequest.txRequestId, + }); +} + +async function nockSendTxRequest(txRequest: TxRequest, requestType: RequestType = RequestType.tx): Promise { + const route = requestType === RequestType.message ? '/messages/0' : '/transactions/0'; + return nock('https://bitgo.fakeurl') + .post(`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}${route}/send`) + .reply(200, { + txRequestId: txRequest.txRequestId, + }); +} diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index f93984e8cf..8397b9c28f 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -6,15 +6,27 @@ import { EddsaMPCv2KeyGenRound1Response, EddsaMPCv2KeyGenRound2Request, EddsaMPCv2KeyGenRound2Response, + EddsaMPCv2SignatureShareRound1Output, + EddsaMPCv2SignatureShareRound2Output, MPCv2KeyGenStateEnum, MPCv2PartyFromStringOrNumber, } from '@bitgo/public-types'; -import { EddsaMPSDkg, MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc'; +import { EddsaMPSDkg, EddsaMPSDsg, MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc'; import { KeychainsTriplet } from '../../../baseCoin'; import { AddKeychainOptions, Keychain, KeyType } from '../../../keychain'; import { envRequiresBitgoPubGpgKeyConfig, isBitgoEddsaMpcv2PubKey } from '../../../tss/bitgoPubKeys'; +import { getTxRequest, sendSignatureShareV2, sendTxRequest } from '../../../tss/common'; +import { decodeWithCodec } from '../../codecs'; +import { + getEddsaSignatureShareRound1, + getEddsaSignatureShareRound2, + getEddsaSignatureShareRound3, + verifyBitGoEddsaMessageRound1, + verifyBitGoEddsaMessageRound2, +} from '../../../tss/eddsa/eddsaMPCv2'; import { generateGPGKeyPair } from '../../opengpgUtils'; import { MPCv2PartiesEnum } from '../ecdsa/typesMPCv2'; +import { RequestType, TSSParamsForMessageWithPrv, TSSParamsWithPrv, TxRequest } from '../baseTypes'; import { BaseEddsaUtils } from './base'; import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './eddsaMPCv2KeyGenSender'; @@ -293,4 +305,196 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { ): Promise { return senderFn(MPCv2KeyGenStateEnum['MPCv2-R2'], payload); } + + // #endregion + + // #region sign tx request + + /** + * Signs the transaction associated with the transaction request. + * @param {string | TxRequest} params.txRequest - transaction request object or id + * @param {string} params.prv - decrypted private key + * @param {string} params.reqId - request id + * @returns {Promise} fully signed TxRequest + */ + async signTxRequest(params: TSSParamsWithPrv): Promise { + this.bitgo.setRequestTracer(params.reqId); + return this.signRequestBase(params, RequestType.tx); + } + + /** + * Signs the message associated with the transaction request. + * @param {string | TxRequest} params.txRequest - transaction request object or id + * @param {string} params.prv - decrypted private key + * @param {string} params.reqId - request id + * @returns {Promise} fully signed TxRequest + */ + async signTxRequestForMessage(params: TSSParamsForMessageWithPrv): Promise { + this.bitgo.setRequestTracer(params.reqId); + return this.signRequestBase(params, RequestType.message); + } + + /** + * Full 3-round EdDSA MPCv2 (MPS) online signing orchestration. + * + * Protocol overview: + * WASM Round 0 → getFirstMessage() (produces userMsg1) + * API Round 1 → send userMsg1 / recv bitgoMsg1 + * WASM Round 1 → handleIncomingMessages([userMsg1, bitgoMsg1]) (produces userMsg2) + * API Round 2 → send userMsg2 / recv bitgoMsg2 + * WASM Round 2 → handleIncomingMessagexs([userMsg2, bitgoMsg2]) (produces userMsg3) + * API Round 3 → send userMsg3 (WP finalises; no BitGo msg returned to client) + */ + private async signRequestBase( + params: TSSParamsWithPrv | TSSParamsForMessageWithPrv, + requestType: RequestType + ): Promise { + const userKeyShare = Buffer.from(params.prv, 'base64'); + + const txRequest: TxRequest = + typeof params.txRequest === 'string' + ? await getTxRequest(this.bitgo, this.wallet.id(), params.txRequest, params.reqId) + : params.txRequest; + + let txOrMessageToSign; + let derivationPath; + let bufferContent; + // One fresh ed25519 GPG key pair per signing session, reused across all signing rounds. + const userGpgKey = await generateGPGKeyPair('ed25519'); + const userGpgPrvKey = await pgp.readPrivateKey({ armoredKey: userGpgKey.privateKey }); + const bitgoGpgPubKey = await this.pickBitgoPubGpgKeyForSigning(true, params.reqId, txRequest.enterpriseId, true); + + if (!bitgoGpgPubKey) { + throw new Error('Missing BitGo GPG key for MPCv2'); + } + + if (requestType === RequestType.tx) { + assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest'); + const unsignedTx = + txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs![0]; + txOrMessageToSign = unsignedTx.signableHex; + derivationPath = unsignedTx.derivationPath; + bufferContent = Buffer.from(txOrMessageToSign, 'hex'); + assert(txOrMessageToSign, 'Missing signableHex in unsignedTx'); + await this.baseCoin.verifyTransaction({ + txPrebuild: { txHex: unsignedTx.serializedTxHex ?? txOrMessageToSign }, + txParams: params.txParams || { recipients: [] }, + wallet: this.wallet, + walletType: this.wallet.multisigType(), + }); + } else if (requestType === RequestType.message) { + assert(txRequest.messages && txRequest.messages.length > 0, 'Unable to find messages in txRequest'); + txOrMessageToSign = txRequest.messages[0].messageEncoded; + assert(txOrMessageToSign, 'Missing messageEncoded in messages[0]'); + derivationPath = txRequest.messages[0].derivationPath || 'm/0'; + bufferContent = Buffer.from(txOrMessageToSign, 'hex'); + } else { + throw new Error('Invalid request type'); + } + + // ── WASM Round 0 ────────────────────────────────────────────────────────── + const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER); + userDsg.initDsg(userKeyShare, bufferContent, derivationPath, MPCv2PartiesEnum.BITGO); + const userMsg1 = userDsg.getFirstMessage(); + + // ── API Round 1 ─────────────────────────────────────────────────────────── + const signatureShareRound1 = await getEddsaSignatureShareRound1(userMsg1, userGpgPrvKey); + let latestTxRequest = await sendSignatureShareV2( + this.bitgo, + txRequest.walletId, + txRequest.txRequestId, + [signatureShareRound1], + requestType, + this.baseCoin.getMPCAlgorithm(), + userGpgKey.publicKey, + undefined, + this.wallet.multisigTypeVersion(), + params.reqId + ); + + assert(latestTxRequest.transactions || latestTxRequest.messages, 'Invalid txRequest object after round 1'); + + const signatureShares1 = + requestType === RequestType.tx + ? latestTxRequest.transactions![0].signatureShares + : latestTxRequest.messages![0].signatureShares; + + const parsedBitGoToUserSigShareRoundOne = decodeWithCodec( + EddsaMPCv2SignatureShareRound1Output, + JSON.parse(signatureShares1[signatureShares1.length - 1].share), + 'Unexpected signature share response. Unable to parse data.' + ); + + if (parsedBitGoToUserSigShareRoundOne.type !== 'round1Output') { + throw new Error('Unexpected signature share response. Unable to parse data.'); + } + + const bitgoDeserializedMsg1 = await verifyBitGoEddsaMessageRound1( + parsedBitGoToUserSigShareRoundOne, + bitgoGpgPubKey + ); + + // ── WASM Round 1 ────────────────────────────────────────────────────────── + const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]); + + // ── API Round 2 ─────────────────────────────────────────────────────────── + const signatureShareRoundTwo = await getEddsaSignatureShareRound2(userMsg2, userGpgPrvKey); + latestTxRequest = await sendSignatureShareV2( + this.bitgo, + txRequest.walletId, + txRequest.txRequestId, + [signatureShareRoundTwo], + requestType, + this.baseCoin.getMPCAlgorithm(), + userGpgKey.publicKey, + undefined, + this.wallet.multisigTypeVersion(), + params.reqId + ); + + assert(latestTxRequest.transactions || latestTxRequest.messages, 'Invalid txRequest object after round 2'); + + const txRequestSignatureShares = + requestType === RequestType.tx + ? latestTxRequest.transactions![0].signatureShares + : latestTxRequest.messages![0].signatureShares; + + const parsedBitGoToUserSigShareRoundTwo = decodeWithCodec( + EddsaMPCv2SignatureShareRound2Output, + JSON.parse(txRequestSignatureShares[txRequestSignatureShares.length - 1].share), + 'Unexpected signature share response. Unable to parse data.' + ); + + if (parsedBitGoToUserSigShareRoundTwo.type !== 'round2Output') { + throw new Error('Unexpected signature share response. Unable to parse data.'); + } + + const bitgoDeserializedMsg2 = await verifyBitGoEddsaMessageRound2( + parsedBitGoToUserSigShareRoundTwo, + bitgoGpgPubKey + ); + + // ── WASM Round 2 ────────────────────────────────────────────────────────── + const [userMsg3] = userDsg.handleIncomingMessages([userMsg2, bitgoDeserializedMsg2]); + + // ── API Round 3 ─────────────────────────────────────────────────────────── + // No BitGo response to verify; WP finalises the signing server-side + const signatureShareRound3 = await getEddsaSignatureShareRound3(userMsg3, userGpgPrvKey); + await sendSignatureShareV2( + this.bitgo, + txRequest.walletId, + txRequest.txRequestId, + [signatureShareRound3], + requestType, + this.baseCoin.getMPCAlgorithm(), + userGpgKey.publicKey, + undefined, + this.wallet.multisigTypeVersion(), + params.reqId + ); + + return sendTxRequest(this.bitgo, txRequest.walletId, txRequest.txRequestId, requestType, params.reqId); + } + + // #endregion }