From 3911af8a8189409b30fb04811e81ec52ec93d01b Mon Sep 17 00:00:00 2001 From: Vibhav Simha G Date: Tue, 13 Oct 2026 16:42:45 +0530 Subject: [PATCH] feat: add eddsa mpcv2 custom signers in master express Add createEddsaMPCv2CustomSigners() to master express eddsa.ts, mirroring the ECDSA MPCv2 custom signer pattern. Stateful round generator closures call the existing signMPCv2Round1/2/3() client methods with curveType 'eddsa', capture round 1/2 responses, and forward the encrypted data key, session state and user GPG prv key into the next round. Precondition failures are explicit for out-of-order round calls. Ticket: WCI-925 --- .../api/master/eddsaMpcv2Signing.test.ts | 190 ++++++++++++++++++ src/masterBitgoExpress/handlers/eddsa.ts | 81 ++++++++ 2 files changed, 271 insertions(+) create mode 100644 src/__tests__/api/master/eddsaMpcv2Signing.test.ts diff --git a/src/__tests__/api/master/eddsaMpcv2Signing.test.ts b/src/__tests__/api/master/eddsaMpcv2Signing.test.ts new file mode 100644 index 00000000..93869c67 --- /dev/null +++ b/src/__tests__/api/master/eddsaMpcv2Signing.test.ts @@ -0,0 +1,190 @@ +import 'should'; + +import { TxRequest } from '@bitgo-beta/sdk-core'; +import { + AdvancedWalletManagerClient, + SignMpcV2Round1Response, + SignMpcV2Round2Response, +} from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; +import { createEddsaMPCv2CustomSigners } from '../../../masterBitgoExpress/handlers/eddsa'; + +describe('createEddsaMPCv2CustomSigners', () => { + const source = 'user' as const; + const commonKeychain = 'mock-common-keychain'; + + const txRequestFixture = { txRequestId: '123456' } as unknown as TxRequest; // minimal fixture: generators forward txRequest opaquely + + const mockRound1Response: SignMpcV2Round1Response = { + signatureShareRound1: { from: 'user', to: 'bitgo', share: 'mock-round1-share' }, + userGpgPubKey: 'mock-user-gpg-pub-key', + encryptedRound1Session: 'mock-encrypted-round1-session', + encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', + encryptedDataKey: 'mock-encrypted-data-key', + }; + const mockRound2Response: SignMpcV2Round2Response = { + signatureShareRound2: { from: 'user', to: 'bitgo', share: 'mock-round2-share' }, + encryptedRound2Session: 'mock-encrypted-round2-session', + }; + const mockRound3Response = { + signatureShareRound3: { from: 'user', to: 'bitgo', share: 'mock-round3-share' }, + }; + + type AwmCall = { + method: string; + source: string; + commonKeychain: string; + params: Record; + }; + + let awmCalls: AwmCall[]; + let awmClient: AdvancedWalletManagerClient; + + beforeEach(() => { + awmCalls = []; + // Minimal stub: only the three MPCv2 sign methods are exercised here. + awmClient = { + signMPCv2Round1: async ( + source: 'user' | 'backup', + commonKeychain: string, + params: Record, + ) => { + awmCalls.push({ method: 'signMPCv2Round1', source, commonKeychain, params }); + return mockRound1Response; + }, + signMPCv2Round2: async ( + source: 'user' | 'backup', + commonKeychain: string, + params: Record, + ) => { + awmCalls.push({ method: 'signMPCv2Round2', source, commonKeychain, params }); + return mockRound2Response; + }, + signMPCv2Round3: async ( + source: 'user' | 'backup', + commonKeychain: string, + params: Record, + ) => { + awmCalls.push({ method: 'signMPCv2Round3', source, commonKeychain, params }); + return mockRound3Response; + }, + } as unknown as AdvancedWalletManagerClient; + }); + + it('round 1 generator calls AWM with curveType eddsa', async () => { + const { customEddsaMPCv2Round1Generator } = createEddsaMPCv2CustomSigners( + awmClient, + source, + commonKeychain, + ); + + const response = await customEddsaMPCv2Round1Generator({ txRequest: txRequestFixture }); + + response.should.equal(mockRound1Response); + awmCalls.should.have.length(1); + awmCalls[0].method.should.equal('signMPCv2Round1'); + awmCalls[0].source.should.equal(source); + awmCalls[0].commonKeychain.should.equal(commonKeychain); + awmCalls[0].params.should.have.property('curveType', 'eddsa'); + awmCalls[0].params.should.have.property('txRequest', txRequestFixture); + }); + + it('round 2 generator forwards round 1 encrypted state and curveType eddsa', async () => { + const { customEddsaMPCv2Round1Generator, customEddsaMPCv2Round2Generator } = + createEddsaMPCv2CustomSigners(awmClient, source, commonKeychain); + + await customEddsaMPCv2Round1Generator({ txRequest: txRequestFixture }); + const response = await customEddsaMPCv2Round2Generator({ + txRequest: txRequestFixture, + bitgoPublicGpgKey: 'mock-bitgo-gpg-key', + encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', + encryptedRound1Session: 'mock-encrypted-round1-session', + }); + + response.should.equal(mockRound2Response); + awmCalls.should.have.length(2); + awmCalls[1].method.should.equal('signMPCv2Round2'); + awmCalls[1].params.should.have.property('curveType', 'eddsa'); + awmCalls[1].params.should.have.property( + 'encryptedDataKey', + mockRound1Response.encryptedDataKey, + ); + awmCalls[1].params.should.have.property( + 'encryptedRound1Session', + mockRound1Response.encryptedRound1Session, + ); + awmCalls[1].params.should.have.property( + 'encryptedUserGpgPrvKey', + mockRound1Response.encryptedUserGpgPrvKey, + ); + awmCalls[1].params.should.have.property('bitgoPublicGpgKey', 'mock-bitgo-gpg-key'); + }); + + it('round 3 generator forwards round 1 + round 2 encrypted state and curveType eddsa', async () => { + const { + customEddsaMPCv2Round1Generator, + customEddsaMPCv2Round2Generator, + customEddsaMPCv2Round3Generator, + } = createEddsaMPCv2CustomSigners(awmClient, source, commonKeychain); + + await customEddsaMPCv2Round1Generator({ txRequest: txRequestFixture }); + await customEddsaMPCv2Round2Generator({ + txRequest: txRequestFixture, + bitgoPublicGpgKey: 'mock-bitgo-gpg-key', + encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', + encryptedRound1Session: 'mock-encrypted-round1-session', + }); + const response = await customEddsaMPCv2Round3Generator({ + txRequest: txRequestFixture, + bitgoPublicGpgKey: 'mock-bitgo-gpg-key', + encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', + encryptedRound2Session: 'mock-encrypted-round2-session', + }); + + response.should.equal(mockRound3Response); + awmCalls.should.have.length(3); + awmCalls[2].method.should.equal('signMPCv2Round3'); + awmCalls[2].params.should.have.property('curveType', 'eddsa'); + awmCalls[2].params.should.have.property( + 'encryptedDataKey', + mockRound1Response.encryptedDataKey, + ); + awmCalls[2].params.should.have.property( + 'encryptedRound2Session', + mockRound2Response.encryptedRound2Session, + ); + awmCalls[2].params.should.have.property( + 'encryptedUserGpgPrvKey', + mockRound1Response.encryptedUserGpgPrvKey, + ); + }); + + it('round 2 before round 1 throws the expected error', async () => { + const { customEddsaMPCv2Round2Generator } = createEddsaMPCv2CustomSigners( + awmClient, + source, + commonKeychain, + ); + + await customEddsaMPCv2Round2Generator({ + txRequest: txRequestFixture, + bitgoPublicGpgKey: 'mock-bitgo-gpg-key', + encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', + encryptedRound1Session: 'mock-encrypted-round1-session', + }).should.be.rejectedWith('Round 1 must be completed before Round 2'); + awmCalls.should.have.length(0); + }); + + it('round 3 before round 2 throws the expected error', async () => { + const { customEddsaMPCv2Round1Generator, customEddsaMPCv2Round3Generator } = + createEddsaMPCv2CustomSigners(awmClient, source, commonKeychain); + + await customEddsaMPCv2Round1Generator({ txRequest: txRequestFixture }); + await customEddsaMPCv2Round3Generator({ + txRequest: txRequestFixture, + bitgoPublicGpgKey: 'mock-bitgo-gpg-key', + encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', + encryptedRound2Session: 'mock-encrypted-round2-session', + }).should.be.rejectedWith('Round 2 must be completed before Round 3'); + awmCalls.should.have.length(1); + }); +}); diff --git a/src/masterBitgoExpress/handlers/eddsa.ts b/src/masterBitgoExpress/handlers/eddsa.ts index 3ea83c85..0a1006f7 100644 --- a/src/masterBitgoExpress/handlers/eddsa.ts +++ b/src/masterBitgoExpress/handlers/eddsa.ts @@ -9,6 +9,9 @@ import { SignShare, SignatureShareRecord, CustomCommitmentGeneratingFunction, + CustomEddsaMPCv2SigningRound1GeneratingFunction, + CustomEddsaMPCv2SigningRound2GeneratingFunction, + CustomEddsaMPCv2SigningRound3GeneratingFunction, CustomRShareGeneratingFunction, CustomGShareGeneratingFunction, ShareKeyPosition, @@ -16,6 +19,8 @@ import { import { AdvancedWalletManagerClient, SignMpcCommitmentResponse, + SignMpcV2Round1Response, + SignMpcV2Round2Response, } from '../clients/advancedWalletManagerClient'; /** @@ -99,6 +104,82 @@ export function createEddsaCustomSigningFunctions( }; } +/** + * Creates custom EdDSA MPCv2 signing functions for use with advanced wallet manager client + */ +export function createEddsaMPCv2CustomSigners( + awmClient: AdvancedWalletManagerClient, + source: 'user' | 'backup', + commonKeychain: string, +): { + customEddsaMPCv2Round1Generator: CustomEddsaMPCv2SigningRound1GeneratingFunction; + customEddsaMPCv2Round2Generator: CustomEddsaMPCv2SigningRound2GeneratingFunction; + customEddsaMPCv2Round3Generator: CustomEddsaMPCv2SigningRound3GeneratingFunction; +} { + // Create state to maintain data between rounds + let round1Response: SignMpcV2Round1Response; + let round2Response: SignMpcV2Round2Response; + + // Create custom signing methods that maintain state + const customEddsaMPCv2Round1Generator: CustomEddsaMPCv2SigningRound1GeneratingFunction = + async (params: { txRequest: TxRequest }) => { + const response = await awmClient.signMPCv2Round1(source, commonKeychain, { + ...params, + curveType: 'eddsa', + }); + round1Response = response; + return response; + }; + + const customEddsaMPCv2Round2Generator: CustomEddsaMPCv2SigningRound2GeneratingFunction = + async (params: { + txRequest: TxRequest; + bitgoPublicGpgKey: string; + encryptedUserGpgPrvKey: string; + encryptedRound1Session: string; + }) => { + if (!round1Response) { + throw new Error('Round 1 must be completed before Round 2'); + } + const response = await awmClient.signMPCv2Round2(source, commonKeychain, { + ...params, + curveType: 'eddsa', + encryptedDataKey: round1Response.encryptedDataKey, + encryptedRound1Session: round1Response.encryptedRound1Session, + encryptedUserGpgPrvKey: round1Response.encryptedUserGpgPrvKey, + bitgoPublicGpgKey: params.bitgoPublicGpgKey, + }); + round2Response = response; + return response; + }; + + const customEddsaMPCv2Round3Generator: CustomEddsaMPCv2SigningRound3GeneratingFunction = + async (params: { + txRequest: TxRequest; + bitgoPublicGpgKey: string; + encryptedUserGpgPrvKey: string; + encryptedRound2Session: string; + }) => { + if (!round2Response) { + throw new Error('Round 2 must be completed before Round 3'); + } + return await awmClient.signMPCv2Round3(source, commonKeychain, { + ...params, + curveType: 'eddsa', + encryptedDataKey: round1Response.encryptedDataKey, + encryptedRound2Session: round2Response.encryptedRound2Session, + encryptedUserGpgPrvKey: round1Response.encryptedUserGpgPrvKey, + bitgoPublicGpgKey: params.bitgoPublicGpgKey, + }); + }; + + return { + customEddsaMPCv2Round1Generator, + customEddsaMPCv2Round2Generator, + customEddsaMPCv2Round3Generator, + }; +} + export async function handleEddsaSigning( bitgo: BitGoBase, wallet: Wallet,