diff --git a/src/__tests__/api/master/eddsa.test.ts b/src/__tests__/api/master/eddsa.test.ts index 8b3b43d..35ae812 100644 --- a/src/__tests__/api/master/eddsa.test.ts +++ b/src/__tests__/api/master/eddsa.test.ts @@ -17,7 +17,7 @@ import { BitGo } from 'bitgo'; import { readKey } from 'openpgp'; // TODO: Re-enable once using EDDSA Custom signing fns -xdescribe('Eddsa Signing Handler', () => { +describe('Eddsa Signing Handler', () => { let bitgo: BitGoBase; let wallet: Wallet; let enclavedExpressClient: EnclavedExpressClient; @@ -105,30 +105,6 @@ xdescribe('Eddsa Signing Handler', () => { const pgpKey = await readKey({ armoredKey: bitgoGpgKey.publicKey }); sinon.stub(EddsaUtils.prototype, 'getBitgoPublicGpgKey').resolves(pgpKey); - // Mock getTxRequest call - const getTxRequestNock = nock(bitgoApiUrl) - .get(`/api/v2/wallet/${walletId}/txrequests`) - .query({ txRequestIds: 'test-tx-request-id', latest: true }) - .matchHeader('any', () => true) - .reply(200, { - txRequests: [ - { - txRequestId: 'test-tx-request-id', - state: 'signed', - apiVersion: 'full', - pendingApprovalId: 'test-pending-approval-id', - transactions: [ - { - unsignedTx: { - derivationPath: 'm/0', - signableHex: 'testMessage', - }, - }, - ], - }, - ], - }); - // Mock exchangeEddsaCommitments call const exchangeCommitmentsNock = nock(bitgoApiUrl) .post(`/api/v2/wallet/${walletId}/txrequests/test-tx-request-id/transactions/0/commit`) @@ -266,7 +242,6 @@ xdescribe('Eddsa Signing Handler', () => { state: 'signed', }); - getTxRequestNock.done(); exchangeCommitmentsNock.done(); offerRShareNock.done(); getBitgoRShareNock.done(); diff --git a/src/__tests__/api/master/sendMany.test.ts b/src/__tests__/api/master/sendMany.test.ts index 74f5e23..d6e273f 100644 --- a/src/__tests__/api/master/sendMany.test.ts +++ b/src/__tests__/api/master/sendMany.test.ts @@ -8,7 +8,6 @@ import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; import { Environments, Wallet } from '@bitgo/sdk-core'; import { Coin } from 'bitgo'; import assert from 'assert'; -import * as eddsa from '../../../api/master/handlers/eddsa'; describe('POST /api/:coin/wallet/:walletId/sendmany', () => { let agent: request.SuperAgentTest; @@ -228,25 +227,6 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => { describe('SendMany TSS EDDSA:', () => { const coin = 'tsol'; it('should send many transactions using EDDSA TSS signing', async () => { - const mockTxRequest = { - txRequestId: 'test-tx-request-id', - state: 'signed', - apiVersion: 'full', - pendingApprovalId: 'test-pending-approval-id', - transactions: [ - { - unsignedTx: { - derivationPath: 'm/0', - signableHex: 'testMessage', - }, - signedTx: { - id: 'test-tx-id', - tx: 'signed-transaction', - }, - }, - ], - }; - // Mock wallet get request for TSS wallet const walletGetNock = nock(bitgoApiUrl) .get(`/api/v2/${coin}/wallet/${walletId}`) @@ -270,40 +250,35 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => { source: 'user', type: 'tss', }); - - const prebuildStub = sinon.stub(Wallet.prototype, 'prebuildTransaction').resolves({ - txRequestId: 'test-tx-request-id', - txHex: 'prebuilt-tx-hex', - txInfo: { - nP2SHInputs: 1, - nSegwitInputs: 0, - nOutputs: 2, + const sendManyStub = sinon.stub(Wallet.prototype, 'sendMany').resolves({ + txRequest: { + txRequestId: 'test-tx-request-id', + state: 'signed', + apiVersion: 'full', + pendingApprovalId: 'test-pending-approval-id', + transactions: [ + { + state: 'signed', + unsignedTx: { + derivationPath: 'm/0', + signableHex: 'testMessage', + serializedTxHex: 'testSerializedTxHex', + }, + signatureShares: [], + signedTx: { + id: 'test-tx-id', + tx: 'signed-transaction', + }, + }, + ], }, - walletId, + txid: 'test-tx-id', + tx: 'signed-transaction', }); - const verifyStub = sinon.stub(Coin.Tsol.prototype, 'verifyTransaction').resolves(true); - // Mock multisigType to return 'tss' const multisigTypeStub = sinon.stub(Wallet.prototype, 'multisigType').returns('tss'); - // Mock handleEddsaSigning - const handleEddsaSigningStub = sinon.stub().resolves({ - ...mockTxRequest, - }); - - // Import and stub the signAndSendTxRequests function - sinon.stub(eddsa, 'handleEddsaSigning').callsFake(handleEddsaSigningStub); - - // Mock getTxRequest call - const getTxRequestNock = nock(bitgoApiUrl) - .get(`/api/v2/wallet/${walletId}/txrequests`) - .query({ txRequestIds: 'test-tx-request-id', latest: true }) - .matchHeader('any', () => true) - .reply(200, { - txRequests: [mockTxRequest], - }); - const response = await agent .post(`/api/${coin}/wallet/${walletId}/sendMany`) .set('Authorization', `Bearer ${accessToken}`) @@ -329,11 +304,8 @@ describe('POST /api/:coin/wallet/:walletId/sendmany', () => { walletGetNock.done(); keychainGetNock.done(); - sinon.assert.calledOnce(prebuildStub); - sinon.assert.calledOnce(verifyStub); - sinon.assert.calledThrice(multisigTypeStub); - sinon.assert.calledOnce(handleEddsaSigningStub); - getTxRequestNock.done(); + sinon.assert.calledOnce(sendManyStub); + sinon.assert.calledOnce(multisigTypeStub); }); }); diff --git a/src/api/master/clients/enclavedExpressClient.ts b/src/api/master/clients/enclavedExpressClient.ts index 99fd15f..0c150bd 100644 --- a/src/api/master/clients/enclavedExpressClient.ts +++ b/src/api/master/clients/enclavedExpressClient.ts @@ -95,7 +95,7 @@ interface SignMpcCommitmentParams { pub: string; } -interface SignMpcCommitmentResponse { +export interface SignMpcCommitmentResponse { userToBitgoCommitment: CommitmentShareRecord; encryptedSignerShare: EncryptedSignerShareRecord; encryptedUserToBitgoRShare: EncryptedSignerShareRecord; diff --git a/src/api/master/handlers/eddsa.ts b/src/api/master/handlers/eddsa.ts index 6a1d3e9..24f0eb6 100644 --- a/src/api/master/handlers/eddsa.ts +++ b/src/api/master/handlers/eddsa.ts @@ -1,19 +1,97 @@ import { BitGoBase, - getTxRequest, - offerUserToBitgoRShare, - getBitgoToUserRShare, - sendUserToBitgoGShare, Wallet, IRequestTracer, EddsaUtils, BaseCoin, ApiKeyShare, TxRequest, + CommitmentShareRecord, + EncryptedSignerShareRecord, + SignShare, + SignatureShareRecord, + CustomCommitmentGeneratingFunction, + CustomRShareGeneratingFunction, + CustomGShareGeneratingFunction, } from '@bitgo/sdk-core'; -import { EnclavedExpressClient } from '../clients/enclavedExpressClient'; -import { exchangeEddsaCommitments } from '@bitgo/sdk-core/dist/src/bitgo/tss/common'; -import logger from '../../../logger'; +import { EnclavedExpressClient, SignMpcCommitmentResponse } from '../clients/enclavedExpressClient'; + +/** + * Creates custom EdDSA signing functions for use with enclaved express client + */ +export function createEddsaCustomSigningFunctions( + enclavedExpressClient: EnclavedExpressClient, + source: 'user' | 'backup', + commonKeychain: string, +): { + customCommitmentGenerator: CustomCommitmentGeneratingFunction; + customRShareGenerator: CustomRShareGeneratingFunction; + customGShareGenerator: CustomGShareGeneratingFunction; +} { + // Create state to maintain data between rounds + let commitmentResponse: SignMpcCommitmentResponse; + + // Create custom signing methods that maintain state + const customCommitmentGenerator: CustomCommitmentGeneratingFunction = async (params: { + txRequest: TxRequest; + bitgoGpgPubKey?: string; + }) => { + if (!params.bitgoGpgPubKey) { + throw new Error('bitgoGpgPubKey is required for commitment share generation'); + } + const response = await enclavedExpressClient.signMpcCommitment({ + txRequest: params.txRequest, + bitgoPublicGpgKey: params.bitgoGpgPubKey, + source, + pub: commonKeychain, + }); + commitmentResponse = response; + return response; + }; + + const customRShareGenerator: CustomRShareGeneratingFunction = async (params: { + txRequest: TxRequest; + encryptedUserToBitgoRShare: EncryptedSignerShareRecord; + }) => { + if (!commitmentResponse) { + throw new Error('Commitment must be completed before R-share generation'); + } + const response = await enclavedExpressClient.signMpcRShare({ + txRequest: params.txRequest, + encryptedUserToBitgoRShare: params.encryptedUserToBitgoRShare, + encryptedDataKey: commitmentResponse.encryptedDataKey, + source, + pub: commonKeychain, + }); + return { rShare: response.rShare }; + }; + + const customGShareGenerator: CustomGShareGeneratingFunction = async (params: { + txRequest: TxRequest; + userToBitgoRShare: SignShare; + bitgoToUserRShare: SignatureShareRecord; + bitgoToUserCommitment: CommitmentShareRecord; + }) => { + if (!commitmentResponse) { + throw new Error('Commitment must be completed before G-share generation'); + } + const response = await enclavedExpressClient.signMpcGShare({ + txRequest: params.txRequest, + bitgoToUserRShare: params.bitgoToUserRShare, + userToBitgoRShare: params.userToBitgoRShare, + bitgoToUserCommitment: params.bitgoToUserCommitment, + source, + pub: commonKeychain, + }); + return response.gShare; + }; + + return { + customCommitmentGenerator, + customRShareGenerator, + customGShareGenerator, + }; +} export async function handleEddsaSigning( bitgo: BitGoBase, @@ -24,70 +102,15 @@ export async function handleEddsaSigning( reqId?: IRequestTracer, ) { const eddsaUtils = new EddsaUtils(bitgo, wallet.baseCoin, wallet); - - const { apiVersion } = txRequest; - const bitgoGpgKey = await eddsaUtils.getBitgoPublicGpgKey(); - - const { - userToBitgoCommitment, - encryptedSignerShare, - encryptedUserToBitgoRShare, - encryptedDataKey, - } = await enclavedExpressClient.signMpcCommitment({ + const { customCommitmentGenerator, customRShareGenerator, customGShareGenerator } = + createEddsaCustomSigningFunctions(enclavedExpressClient, 'user', commonKeychain); + return await eddsaUtils.signEddsaTssUsingExternalSigner( txRequest, - bitgoPublicGpgKey: bitgoGpgKey.armor(), - source: 'user', - pub: commonKeychain, - }); - - const { commitmentShare: bitgoToUserCommitment } = await exchangeEddsaCommitments( - bitgo, - wallet.id(), - txRequest.txRequestId, - userToBitgoCommitment, - encryptedSignerShare, - apiVersion, - reqId, - ); - - const { rShare } = await enclavedExpressClient.signMpcRShare({ - txRequest, - encryptedUserToBitgoRShare, - encryptedDataKey, - source: 'user', - pub: commonKeychain, - }); - - await offerUserToBitgoRShare( - bitgo, - wallet.id(), - txRequest.txRequestId, - rShare, - encryptedSignerShare.share, - apiVersion, + customCommitmentGenerator, + customRShareGenerator, + customGShareGenerator, reqId, ); - const bitgoToUserRShare = await getBitgoToUserRShare( - bitgo, - wallet.id(), - txRequest.txRequestId, - reqId, - ); - const gSignShareTransactionParams = { - txRequest, - bitgoToUserRShare: bitgoToUserRShare, - userToBitgoRShare: rShare, - bitgoToUserCommitment, - }; - const { gShare } = await enclavedExpressClient.signMpcGShare({ - ...gSignShareTransactionParams, - source: 'user', - pub: commonKeychain, - }); - - await sendUserToBitgoGShare(bitgo, wallet.id(), txRequest.txRequestId, gShare, apiVersion, reqId); - logger.debug('Successfully completed signing!'); - return await getTxRequest(bitgo, wallet.id(), txRequest.txRequestId, reqId); } interface OrchestrateEddsaKeyGenParams { diff --git a/src/api/master/handlers/handleSendMany.ts b/src/api/master/handlers/handleSendMany.ts index 46fdc5b..c7fb51b 100644 --- a/src/api/master/handlers/handleSendMany.ts +++ b/src/api/master/handlers/handleSendMany.ts @@ -7,13 +7,12 @@ import { SendManyOptions, PrebuildTransactionResult, Keychain, - getTxRequest, } from '@bitgo/sdk-core'; import logger from '../../../logger'; import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec'; import { createEcdsaMPCv2CustomSigners } from './ecdsaMPCv2'; import { EnclavedExpressClient } from '../clients/enclavedExpressClient'; -import { signAndSendTxRequests } from './transactionRequests'; +import { createEddsaCustomSigningFunctions } from './eddsa'; /** * Defines the structure for a single recipient in a send-many transaction. @@ -31,25 +30,21 @@ interface Recipient { /** * Creates TSS send parameters for ECDSA MPCv2 signing with custom functions */ -function createEcdsaMPCv2SendParams( +function createMPCSendParamsWithCustomSigningFns( req: MasterApiSpecRouteRequest<'v1.wallet.sendMany', 'post'>, - wallet: Wallet, enclavedExpressClient: EnclavedExpressClient, signingKeychain: Keychain, ): SendManyOptions { const coin = req.bitgo.coin(req.params.coin); + const source = signingKeychain.source as 'user' | 'backup'; + const commonKeychain = signingKeychain.commonKeychain; const mpcAlgorithm = coin.getMPCAlgorithm(); - if (mpcAlgorithm === 'ecdsa') { - // For ECDSA MPCv2, we need to create custom signing functions - const source = signingKeychain.source as 'user' | 'backup'; - const commonKeychain = signingKeychain.commonKeychain; - - if (!commonKeychain) { - throw new Error('Common keychain is required for ECDSA MPCv2 signing'); - } + if (!commonKeychain) { + throw new Error('Common keychain is required for MPC signing'); + } - // Use the shared custom signing functions + if (mpcAlgorithm === 'ecdsa') { const { customMPCv2Round1Generator, customMPCv2Round2Generator, customMPCv2Round3Generator } = createEcdsaMPCv2CustomSigners(enclavedExpressClient, source, commonKeychain); @@ -59,10 +54,19 @@ function createEcdsaMPCv2SendParams( customMPCv2SigningRound2GenerationFunction: customMPCv2Round2Generator, customMPCv2SigningRound3GenerationFunction: customMPCv2Round3Generator, }; - } else { - // For non-ECDSA algorithms, return the original parameters - return req.decoded as SendManyOptions; + } else if (mpcAlgorithm === 'eddsa') { + const { customCommitmentGenerator, customRShareGenerator, customGShareGenerator } = + createEddsaCustomSigningFunctions(enclavedExpressClient, source, commonKeychain); + + return { + ...(req.decoded as SendManyOptions), + customCommitmentGeneratingFunction: customCommitmentGenerator, + customRShareGeneratingFunction: customRShareGenerator, + customGShareGeneratingFunction: customGShareGenerator, + }; } + + throw new Error(`Unsupported MPC algorithm: ${mpcAlgorithm}`); } export async function handleSendMany(req: MasterApiSpecRouteRequest<'v1.wallet.sendMany', 'post'>) { @@ -106,23 +110,21 @@ export async function handleSendMany(req: MasterApiSpecRouteRequest<'v1.wallet.s } try { - // Create TSS send parameters with custom signing functions if needed - + // Create MPC send parameters with custom signing functions if (wallet.multisigType() === 'tss') { if (signingKeychain.source === 'backup') { throw new Error('Backup MPC signing not supported for sendMany'); } - if (wallet.baseCoin.getMPCAlgorithm() === 'ecdsa') { - const ecdsaMPCv2SendParams = createEcdsaMPCv2SendParams( - req, - wallet, - enclavedExpressClient, - signingKeychain, - ); - return wallet.sendMany(ecdsaMPCv2SendParams); - } + const mpcSendParams = createMPCSendParamsWithCustomSigningFns( + req, + enclavedExpressClient, + signingKeychain, + ); + return wallet.sendMany(mpcSendParams); } + /** Multisig */ + const prebuildParams: PrebuildTransactionOptions = { ...params, // Convert memo string to Memo object if present @@ -160,31 +162,15 @@ export async function handleSendMany(req: MasterApiSpecRouteRequest<'v1.wallet.s logger.debug('Tx prebuild: %s', JSON.stringify(txPrebuilt, null, 2)); - // Need to branch off for multisig and tss - if (wallet.multisigType() === 'tss') { - if (!txPrebuilt.txRequestId) { - throw new Error('MPC tx not built correctly.'); - } - const txRequest = await getTxRequest(bitgo, wallet.id(), txPrebuilt.txRequestId, reqId); - return signAndSendTxRequests( - bitgo, - wallet, - txRequest, - enclavedExpressClient, - signingKeychain, - reqId, - ); - } else { - return signAndSendMultisig( - wallet, - req.decoded.source, - txPrebuilt, - prebuildParams, - enclavedExpressClient, - signingKeychain, - reqId, - ); - } + return signAndSendMultisig( + wallet, + req.decoded.source, + txPrebuilt, + prebuildParams, + enclavedExpressClient, + signingKeychain, + reqId, + ); } catch (error) { const err = error as Error; logger.error('Failed to send many: %s', err.message);