From c3dee70bd8e78f7e095a15382394148282a3ee4f Mon Sep 17 00:00:00 2001 From: Marzooqa Kather Date: Fri, 8 May 2026 18:07:55 +0000 Subject: [PATCH] refactor(sdk-core): eddsa MPCv2 DSG post-merge nits - Rename verifyBitGoMessageRoundOne/Two -> verifyPeerMessageRoundOne/Two to reflect that the functions accept any peer party, not just BitGo - Unify literal union param types (0|1, 0|1|2) to MPCv2PartiesEnum in getSignatureShareRound{One,Two,Three} and partyIdToSignatureShareType; add SignerPartyId alias enforcing USER|BACKUP for signer params - Remove dead runtime assert in partyIdToSignatureShareType (TypeScript now guarantees valid enum values at compile time) and unused assert import - Replace if/throw GPG key guard in signRequestBase with assert() for consistency with surrounding invariant checks - Add round-2 wrong-type negative test to signTxRequest test suite, symmetric to existing round-1 wrong-type test Ticket: WCI-372 Session-Id: f87490c7-3cae-4e41-8080-cf6fab5f55b4 Task-Id: cf4b9ca6-45e4-4f2a-99a6-898185b4f147 --- .../tssUtils/eddsaMPCv2/signTxRequest.ts | 84 +++++++++++++++++++ .../src/bitgo/tss/eddsa/eddsaMPCv2.ts | 28 +++---- .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 13 ++- .../unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 32 +++---- 4 files changed, 119 insertions(+), 38 deletions(-) diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts index 98ec3a6f0e..3b6bf33dc5 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts @@ -239,6 +239,90 @@ describe('signTxRequest:', function () { .should.be.rejectedWith(/Unexpected signature share response/); }); + it('should throw if round 2 response has wrong type', async function () { + const messageBuffer = Buffer.from(signableHex, 'hex'); + const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO); + bitgoDsg.initDsg( + bitgoKeyShare, + messageBuffer, + txRequest.transactions![0].unsignedTx.derivationPath, + MPCv2PartiesEnum.USER + ); + const bitgoMsg1 = bitgoDsg.getFirstMessage(); + + // Round 1: return a valid round1Output so the orchestration can proceed + nock('https://bitgo.fakeurl') + .post( + `/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}/transactions/0/sign`, + (body) => + (JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound1Input).type === 'round1Input' + ) + .reply( + 200, + async (_uri: string, body: { signatureShares: SignatureShareRecord[]; signerGpgPublicKey: string }) => { + 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), + }; + // Advance bitgo session (we don't need bitgoMsg2 for this test) + bitgoDsg.handleIncomingMessages([bitgoMsg1, userDeserializedMsg1]); + const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoPrvKeyObj); + const round1Output: EddsaMPCv2SignatureShareRound1Output = { + type: 'round1Output', + data: { msg1: bitgoSignedMsg1 }, + }; + return { + txRequestId, + transactions: [ + { + signatureShares: [ + { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify(round1Output), + }, + ], + }, + ], + }; + } + ); + + // Round 2: return a share with wrong type (round3Output instead of round2Output) + nock('https://bitgo.fakeurl') + .post( + `/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}/transactions/0/sign`, + (body) => + (JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound2Input).type === 'round2Input' + ) + .reply(200, { + txRequestId, + transactions: [ + { + signatureShares: [ + { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + share: 'placeholder', + }, + { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify({ type: 'round3Output', data: {} }), + }, + ], + }, + ], + }); + + const userPrvBase64 = Buffer.from(userKeyShare).toString('base64'); + await tssUtils + .signTxRequest({ txRequest, prv: userPrvBase64, reqId, txParams }) + .should.be.rejectedWith(/Unexpected signature share response. Unable to parse data./); + }); + it('successfully signs a txRequest after receiving multiple 429 errors in round 2', async function () { const nockPromises = await getNockPromisesForEddsaSigning(txRequest, RequestType.tx, 3); await Promise.all(nockPromises); diff --git a/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts index 4e0a62ba4f..b90b755ba6 100644 --- a/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts @@ -1,4 +1,3 @@ -import assert from 'assert'; import * as openpgp from 'openpgp'; import { MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc'; import { @@ -11,14 +10,15 @@ import { import { SignatureShareRecord, SignatureShareType } from '../../utils/tss/baseTypes'; import { MPCv2PartiesEnum } from '../../utils/tss/ecdsa/typesMPCv2'; -function partyIdToSignatureShareType(partyId: 0 | 1 | 2): SignatureShareType { - assert(partyId === 0 || partyId === 1 || partyId === 2, 'Invalid partyId for EdDSA MPCv2 signing'); +type SignerPartyId = MPCv2PartiesEnum.USER | MPCv2PartiesEnum.BACKUP; + +function partyIdToSignatureShareType(partyId: MPCv2PartiesEnum): SignatureShareType { switch (partyId) { - case 0: + case MPCv2PartiesEnum.USER: return SignatureShareType.USER; - case 1: + case MPCv2PartiesEnum.BACKUP: return SignatureShareType.BACKUP; - case 2: + case MPCv2PartiesEnum.BITGO: return SignatureShareType.BITGO; } } @@ -32,8 +32,8 @@ function partyIdToSignatureShareType(partyId: 0 | 1 | 2): SignatureShareType { export async function getSignatureShareRoundOne( userMsg1: MPSTypes.DeserializedMessage, userGpgPrivKey: openpgp.PrivateKey, - partyId: 0 | 1 = 0, - otherSignerPartyId: 0 | 1 | 2 = 2 + partyId: SignerPartyId = MPCv2PartiesEnum.USER, + otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO ): Promise { const signedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg1.payload), userGpgPrivKey); const share: EddsaMPCv2SignatureShareRound1Input = { @@ -51,7 +51,7 @@ export async function getSignatureShareRoundOne( * Verifies the peer's round-1 PGP signature and returns the raw deserialized * message ready for `DSG.handleIncomingMessages`. */ -export async function verifyBitGoMessageRoundOne( +export async function verifyPeerMessageRoundOne( parsedRound1Output: EddsaMPCv2SignatureShareRound1Output, peerGpgKey: openpgp.Key, peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO @@ -69,8 +69,8 @@ export async function verifyBitGoMessageRoundOne( export async function getSignatureShareRoundTwo( userMsg2: MPSTypes.DeserializedMessage, userGpgPrivKey: openpgp.PrivateKey, - partyId: 0 | 1 = 0, - otherSignerPartyId: 0 | 1 | 2 = 2 + partyId: SignerPartyId = MPCv2PartiesEnum.USER, + otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO ): Promise { const signedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg2.payload), userGpgPrivKey); const share: EddsaMPCv2SignatureShareRound2Input = { @@ -88,7 +88,7 @@ export async function getSignatureShareRoundTwo( * Verifies the peer's round-2 PGP signature and returns the raw deserialized * message ready for `DSG.handleIncomingMessages`. */ -export async function verifyBitGoMessageRoundTwo( +export async function verifyPeerMessageRoundTwo( parsedRound2Output: EddsaMPCv2SignatureShareRound2Output, peerGpgKey: openpgp.Key, peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO @@ -110,8 +110,8 @@ export async function verifyBitGoMessageRoundTwo( export async function getSignatureShareRoundThree( userMsg3: MPSTypes.DeserializedMessage, userGpgPrivKey: openpgp.PrivateKey, - partyId: 0 | 1 = 0, - otherSignerPartyId: 0 | 1 | 2 = 2 + partyId: SignerPartyId = MPCv2PartiesEnum.USER, + otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO ): Promise { const signedMsg3 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg3.payload), userGpgPrivKey); const share: EddsaMPCv2SignatureShareRound3Input = { 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 1c8cc543cf..6b25c306c5 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -21,8 +21,8 @@ import { getSignatureShareRoundOne, getSignatureShareRoundTwo, getSignatureShareRoundThree, - verifyBitGoMessageRoundOne, - verifyBitGoMessageRoundTwo, + verifyPeerMessageRoundOne, + verifyPeerMessageRoundTwo, } from '../../../tss/eddsa/eddsaMPCv2'; import { generateGPGKeyPair } from '../../opengpgUtils'; import { MPCv2PartiesEnum } from '../ecdsa/typesMPCv2'; @@ -363,10 +363,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { 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'); - } + assert(bitgoGpgPubKey, 'Missing BitGo GPG key for MPCv2'); if (requestType === RequestType.tx) { assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest'); @@ -432,7 +429,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { throw new Error('Unexpected signature share response. Unable to parse data.'); } - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne(parsedBitGoToUserSigShareRoundOne, bitgoGpgPubKey); + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne(parsedBitGoToUserSigShareRoundOne, bitgoGpgPubKey); // ── WASM Round 1 ────────────────────────────────────────────────────────── const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]); @@ -471,7 +468,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { throw new Error('Unexpected signature share response. Unable to parse data.'); } - const bitgoDeserializedMsg2 = await verifyBitGoMessageRoundTwo(parsedBitGoToUserSigShareRoundTwo, bitgoGpgPubKey); + const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo(parsedBitGoToUserSigShareRoundTwo, bitgoGpgPubKey); // ── WASM Round 2 ────────────────────────────────────────────────────────── const [userMsg3] = userDsg.handleIncomingMessages([userMsg2, bitgoDeserializedMsg2]); 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 b228d1be96..6b0c1964cb 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 @@ -13,8 +13,8 @@ import { getSignatureShareRoundOne, getSignatureShareRoundTwo, getSignatureShareRoundThree, - verifyBitGoMessageRoundOne, - verifyBitGoMessageRoundTwo, + verifyPeerMessageRoundOne, + verifyPeerMessageRoundTwo, } from '../../../../../../src/bitgo/tss/eddsa/eddsaMPCv2'; import { decodeWithCodec } from '../../../../../../src/bitgo/utils/codecs'; import { generateGPGKeyPair } from '../../../../../../src/bitgo/utils/opengpgUtils'; @@ -96,7 +96,7 @@ describe('EdDSA MPS DSG helper functions', async () => { assert.ok(parsed.data.msg1.signature, 'msg1.signature should be set'); }); - it('verifyBitGoMessageRoundOne should verify a valid BitGo round-1 message', async () => { + it('verifyPeerMessageRoundOne should verify a valid BitGo round-1 message', async () => { const messageBuffer = Buffer.from(signableHex, 'hex'); const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO); bitgoDsg.initDsg(bitgoKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.USER); @@ -108,13 +108,13 @@ describe('EdDSA MPS DSG helper functions', async () => { data: { msg1: bitgoSignedMsg1 }, }; - const result = await verifyBitGoMessageRoundOne(round1Output, bitgoGpgPubKey); + const result = await verifyPeerMessageRoundOne(round1Output, bitgoGpgPubKey); assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO); assert.ok(result.payload.length > 0, 'payload should be non-empty'); }); - it('verifyBitGoMessageRoundOne should throw on a tampered message', async () => { + it('verifyPeerMessageRoundOne should throw on a tampered message', async () => { const round1Output: EddsaMPCv2SignatureShareRound1Output = { type: 'round1Output', data: { @@ -125,7 +125,7 @@ describe('EdDSA MPS DSG helper functions', async () => { }, }; - await assert.rejects(verifyBitGoMessageRoundOne(round1Output, bitgoGpgPubKey), 'should throw on invalid signature'); + await assert.rejects(verifyPeerMessageRoundOne(round1Output, bitgoGpgPubKey), 'should throw on invalid signature'); }); // ── Round 2 ───────────────────────────────────────────────────────────────── @@ -141,7 +141,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const bitgoMsg1 = bitgoDsg.getFirstMessage(); const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne( + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, bitgoGpgPubKey ); @@ -173,7 +173,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const bitgoMsg1 = bitgoDsg.getFirstMessage(); const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne( + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, bitgoGpgPubKey ); @@ -198,7 +198,7 @@ describe('EdDSA MPS DSG helper functions', async () => { assert.ok(parsed.data.msg2.signature, 'msg2.signature should be set'); }); - it('verifyBitGoMessageRoundTwo should verify a valid BitGo round-2 message', async () => { + it('verifyPeerMessageRoundTwo should verify a valid BitGo round-2 message', async () => { const messageBuffer = Buffer.from(signableHex, 'hex'); const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER); userDsg.initDsg(userKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.BITGO); @@ -216,13 +216,13 @@ describe('EdDSA MPS DSG helper functions', async () => { data: { msg2: bitgoSignedMsg2 }, }; - const result = await verifyBitGoMessageRoundTwo(round2Output, bitgoGpgPubKey); + const result = await verifyPeerMessageRoundTwo(round2Output, bitgoGpgPubKey); assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO); assert.ok(result.payload.length > 0, 'payload should be non-empty'); }); - it('verifyBitGoMessageRoundTwo should throw on a tampered message', async () => { + it('verifyPeerMessageRoundTwo should throw on a tampered message', async () => { const round2Output: EddsaMPCv2SignatureShareRound2Output = { type: 'round2Output', data: { @@ -233,7 +233,7 @@ describe('EdDSA MPS DSG helper functions', async () => { }, }; - await assert.rejects(verifyBitGoMessageRoundTwo(round2Output, bitgoGpgPubKey), 'should throw on invalid signature'); + await assert.rejects(verifyPeerMessageRoundTwo(round2Output, bitgoGpgPubKey), 'should throw on invalid signature'); }); // ── Round 3 ───────────────────────────────────────────────────────────────── @@ -250,7 +250,7 @@ describe('EdDSA MPS DSG helper functions', async () => { // Advance to round 2 const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne( + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, bitgoGpgPubKey ); @@ -258,7 +258,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const [bitgoMsg2] = bitgoDsg.handleIncomingMessages([bitgoMsg1, userMsg1]); const bitgoSignedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg2.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg2 = await verifyBitGoMessageRoundTwo( + const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo( { type: 'round2Output', data: { msg2: bitgoSignedMsg2 } }, bitgoGpgPubKey ); @@ -290,7 +290,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const bitgoMsg1 = bitgoDsg.getFirstMessage(); const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne( + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, bitgoGpgPubKey ); @@ -298,7 +298,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const [bitgoMsg2] = bitgoDsg.handleIncomingMessages([bitgoMsg1, backupMsg1]); const bitgoSignedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg2.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg2 = await verifyBitGoMessageRoundTwo( + const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo( { type: 'round2Output', data: { msg2: bitgoSignedMsg2 } }, bitgoGpgPubKey );