From de2b822b6902e1815742c9cb5cad71fe65f20552 Mon Sep 17 00:00:00 2001 From: Vibhav Simha G Date: Wed, 3 Jun 2026 16:44:16 +0530 Subject: [PATCH] fix(sdk-core): pass round to getBitgoSignatureShare helper Ticket: WCI-626 EdDSA MPCv2 signing can receive multiple BitGo-to-signer shares in a txRequest. Selecting only by sender/recipient can pick a stale round-1 output during round 2. - Add shareType param to getBitgoSignatureShare to filter by round output type. - Update EdDSA MPCv2 callers to request round-1 or round-2 output explicitly. - Remove inline reverse().find() in offline round-3 path in favour of the helper. - Update tests to expect round-specific missing share error messages. --- modules/sdk-core/src/bitgo/tss/common.ts | 20 ++++++--- .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 19 ++------ .../unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 44 ++++++++++++++++++- 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/modules/sdk-core/src/bitgo/tss/common.ts b/modules/sdk-core/src/bitgo/tss/common.ts index 156729d78d..7903038740 100644 --- a/modules/sdk-core/src/bitgo/tss/common.ts +++ b/modules/sdk-core/src/bitgo/tss/common.ts @@ -1,6 +1,7 @@ import assert from 'assert'; import openpgp from 'openpgp'; +import { MPCv2SigningState } from '@bitgo/public-types'; import { BitGoBase } from '../bitgoBase'; import { TxRequestChallengeResponse } from './types'; import { @@ -20,12 +21,21 @@ const debug = require('debug')('bitgo:tss:common'); export function getBitgoSignatureShare( signatureShares: SignatureShareRecord[], - signerShareType: SignatureShareType + signerShareType: SignatureShareType, + shareType: MPCv2SigningState ): SignatureShareRecord { - const bitgoShare = signatureShares.find( - (share) => share.from === SignatureShareType.BITGO && share.to === signerShareType - ); - assert(bitgoShare, 'Missing BitGo signature share'); + const bitgoShare = signatureShares.find((share) => { + if (share.from !== SignatureShareType.BITGO || share.to !== signerShareType) { + return false; + } + + try { + return JSON.parse(share.share).type === shareType; + } catch { + return false; + } + }); + assert(bitgoShare, `Missing BitGo ${shareType} signature share`); return bitgoShare; } 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 8dc3175f48..f11b0f94a5 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -469,7 +469,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { ? latestTxRequest.transactions![0].signatureShares : latestTxRequest.messages![0].signatureShares; - const bitgoShareRoundOne = getBitgoSignatureShare(signatureShares1, signerShareType); + const bitgoShareRoundOne = getBitgoSignatureShare(signatureShares1, signerShareType, 'round1Output'); const parsedBitGoToUserSigShareRoundOne = decodeWithCodec( EddsaMPCv2SignatureShareRound1Output, JSON.parse(bitgoShareRoundOne.share), @@ -508,7 +508,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { ? latestTxRequest.transactions![0].signatureShares : latestTxRequest.messages![0].signatureShares; - const bitgoShareRoundTwo = getBitgoSignatureShare(txRequestSignatureShares, signerShareType); + const bitgoShareRoundTwo = getBitgoSignatureShare(txRequestSignatureShares, signerShareType, 'round2Output'); const parsedBitGoToUserSigShareRoundTwo = decodeWithCodec( EddsaMPCv2SignatureShareRound2Output, JSON.parse(bitgoShareRoundTwo.share), @@ -649,7 +649,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { const signatureShares = transactions[0].signatureShares; assert(signatureShares, 'Missing signature shares in round 1 txRequest'); - const bitgoShareRoundOne = getBitgoSignatureShare(signatureShares, SignatureShareType.USER); + const bitgoShareRoundOne = getBitgoSignatureShare(signatureShares, SignatureShareType.USER, 'round1Output'); const parsedBitGoToUserSigShareRoundOne = decodeWithCodec( EddsaMPCv2SignatureShareRound1Output, JSON.parse(bitgoShareRoundOne.share), @@ -754,18 +754,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { const signatureShares = transactions[0].signatureShares; assert(signatureShares, 'Missing signature shares in round 2 txRequest'); - const bitgoShareRoundTwo = [...signatureShares].reverse().find((share) => { - if (share.from !== SignatureShareType.BITGO || share.to !== SignatureShareType.USER) { - return false; - } - - try { - return JSON.parse(share.share).type === 'round2Output'; - } catch { - return false; - } - }); - assert(bitgoShareRoundTwo, 'Missing BitGo round 2 signature share'); + const bitgoShareRoundTwo = getBitgoSignatureShare(signatureShares, SignatureShareType.USER, 'round2Output'); const parsedBitGoToUserSigShareRoundTwo = decodeWithCodec( EddsaMPCv2SignatureShareRound2Output, diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index 77784805b8..f22ef2cf96 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -32,6 +32,7 @@ import { verifyPeerMessageRoundOne, verifyPeerMessageRoundTwo, } from '../../../../../../src/bitgo/tss/eddsa/eddsaMPCv2'; +import { getBitgoSignatureShare } from '../../../../../../src/bitgo/tss/common'; import { decodeWithCodec } from '../../../../../../src/bitgo/utils/codecs'; import { generateGPGKeyPair } from '../../../../../../src/bitgo/utils/opengpgUtils'; import { MPCv2PartiesEnum } from '../../../../../../src/bitgo/utils/tss/ecdsa/typesMPCv2'; @@ -799,7 +800,7 @@ describe('EddsaMPCv2Utils.createOfflineRound2Share', () => { encryptedUserGpgPrvKey: round1.encryptedUserGpgPrvKey, encryptedRound1Session: round1.encryptedRound1Session, }), - /Missing BitGo signature share/ + /Missing BitGo round1Output signature share/ ); }); @@ -1114,7 +1115,7 @@ describe('EddsaMPCv2Utils.createOfflineRound3Share', () => { encryptedUserGpgPrvKey: round1.encryptedUserGpgPrvKey, encryptedRound2Session: round2.encryptedRound2Session, }), - /Missing BitGo round 2 signature share/ + /Missing BitGo round2Output signature share/ ); }); @@ -1151,6 +1152,45 @@ describe('EddsaMPCv2Utils.createOfflineRound3Share', () => { }); }); +describe('getBitgoSignatureShare', () => { + const round1OutputShare: SignatureShareRecord = { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify({ type: 'round1Output', data: {} }), + }; + const round2OutputShare: SignatureShareRecord = { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify({ type: 'round2Output', data: {} }), + }; + + it('selects the requested BitGo round output when multiple round outputs are present', () => { + const signatureShares = [round1OutputShare, round2OutputShare]; + + assert.strictEqual( + getBitgoSignatureShare(signatureShares, SignatureShareType.USER, 'round2Output'), + round2OutputShare + ); + assert.strictEqual( + getBitgoSignatureShare(signatureShares, SignatureShareType.USER, 'round1Output'), + round1OutputShare + ); + }); + + it('skips malformed share records while selecting the requested BitGo round output', () => { + const malformedShare: SignatureShareRecord = { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: 'not-json', + }; + + assert.strictEqual( + getBitgoSignatureShare([malformedShare, round2OutputShare], SignatureShareType.USER, 'round2Output'), + round2OutputShare + ); + }); +}); + type TxRequestTransaction = NonNullable[number]; function assertSingleTransaction(txRequest: TxRequest): TxRequestTransaction {