diff --git a/src/__tests__/api/advancedWalletManager/signMpcTransaction.test.ts b/src/__tests__/api/advancedWalletManager/signMpcTransaction.test.ts index b4a23081..16b99343 100644 --- a/src/__tests__/api/advancedWalletManager/signMpcTransaction.test.ts +++ b/src/__tests__/api/advancedWalletManager/signMpcTransaction.test.ts @@ -10,7 +10,7 @@ import * as configModule from '../../../initConfig'; import { Ed25519BIP32, Eddsa, SignatureShareType } from '@bitgo-beta/sdk-core'; import { TxRequest } from '@bitgo/public-types'; import { DklsUtils, DklsDsg, DklsTypes } from '@bitgo-beta/sdk-lib-mpc'; -import { EddsaMPCv2Utils } from '@bitgo-beta/sdk-core'; +import { EcdsaMPCv2Utils, EddsaMPCv2Utils } from '@bitgo-beta/sdk-core'; import assert from 'assert'; import { signBitgoMPCv2Round1, signBitgoMPCv2Round2, signBitgoMPCv2Round3 } from './ecdsaUtils'; import { Hash } from 'crypto'; @@ -868,5 +868,111 @@ describe('signMpcTransaction', () => { keyProviderNock.done(); }); + + it('should accept curveType eddsa on mpcv2round1', async () => { + const mockDataKeyResponse = { + plaintextKey: 'mock-plaintext-data-key', + encryptedKey: 'mock-encrypted-data-key', + }; + + const keyProviderNock = nock(keyProviderUrl) + .get(`/key/${mockKeyProviderResponse.pub}`) + .query({ source: 'user' }) + .reply(200, mockKeyProviderResponse); + const dataKeyNock = nock(keyProviderUrl) + .post('/generateDataKey') + .reply(200, mockDataKeyResponse); + + round1ShareStub.resolves(mockEddsaMPCv2Round1Response); + + const response = await agent + .post(`/api/${coin}/mpc/sign/mpcv2round1`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + source: 'user', + pub: mockKeyProviderResponse.pub, + txRequest: mockTxRequest, + curveType: 'eddsa', + }); + + response.status.should.equal(200); + response.body.should.have.property('signatureShareRound1'); + + keyProviderNock.done(); + dataKeyNock.done(); + }); + }); + + describe('MPCv2 curveType compatibility', () => { + let round1ShareStub: sinon.SinonStub; + + beforeEach(() => { + round1ShareStub = sinon.stub(EcdsaMPCv2Utils.prototype, 'createOfflineRound1Share'); + }); + + afterEach(() => { + round1ShareStub.restore(); + }); + + it('should accept curveType ecdsa on hteth mpcv2round1', async () => { + const mockKeyProviderResponse = { + prv: 'mock-ecdsa-private-key', + pub: 'mock-ecdsa-public-key', + source: 'user', + type: 'independent', + }; + const mockDataKeyResponse = { + plaintextKey: 'mock-plaintext-data-key', + encryptedKey: 'mock-encrypted-data-key', + }; + const mockEcdsaMPCv2Round1Response = { + signatureShareRound1: { from: 'user', to: 'bitgo', payload: 'mock-round1-payload' }, + userGpgPubKey: 'mock-ecdsa-user-gpg-pub-key', + encryptedRound1Session: 'mock-ecdsa-encrypted-round1-session', + encryptedUserGpgPrvKey: 'mock-ecdsa-encrypted-user-gpg-prv-key', + }; + + const keyProviderNock = nock(keyProviderUrl) + .get(`/key/${mockKeyProviderResponse.pub}`) + .query({ source: 'user' }) + .reply(200, mockKeyProviderResponse); + const dataKeyNock = nock(keyProviderUrl) + .post('/generateDataKey') + .reply(200, mockDataKeyResponse); + + round1ShareStub.resolves(mockEcdsaMPCv2Round1Response); + + const response = await agent + .post(`/api/hteth/mpc/sign/mpcv2round1`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + source: 'user', + pub: mockKeyProviderResponse.pub, + txRequest: mockTxRequest, + bitgoPublicGpgKey: 'mock-bitgo-gpg-key', + curveType: 'ecdsa', + }); + + response.status.should.equal(200); + response.body.should.have.property('signatureShareRound1'); + + keyProviderNock.done(); + dataKeyNock.done(); + }); + + it('should reject an invalid curveType value on mpcv2round1', async () => { + const response = await agent + .post(`/api/hteth/mpc/sign/mpcv2round1`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ + source: 'user', + pub: 'mock-pub', + txRequest: mockTxRequest, + curveType: 'secp', + }); + + response.status.should.equal(400); + response.body.should.have.property('error'); + }); }); }); diff --git a/src/__tests__/api/master/advancedWalletManagerClient.test.ts b/src/__tests__/api/master/advancedWalletManagerClient.test.ts new file mode 100644 index 00000000..aeb5cbbe --- /dev/null +++ b/src/__tests__/api/master/advancedWalletManagerClient.test.ts @@ -0,0 +1,159 @@ +import 'should'; + +import nock from 'nock'; +import { TxRequest } from '@bitgo-beta/sdk-core'; +import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; +import { AdvancedWalletManagerClient } from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; + +describe('AdvancedWalletManagerClient MPCv2 curveType forwarding', () => { + const awmUrl = 'http://awm.invalid'; + const coin = 'tsol'; + + const cfg: MasterExpressConfig = { + appMode: AppMode.MASTER_EXPRESS, + port: 0, + bind: 'localhost', + timeout: 60000, + httpLoggerFile: '', + env: 'test', + advancedWalletManagerUrl: awmUrl, + tlsMode: TlsMode.DISABLED, + asyncModeConfig: { + enabled: false, + awmAsyncUrl: 'http://awm-async.invalid', + pollIntervalInMs: 1000, + jobTtlInSeconds: 60, + jobTtlMpcInSeconds: 60, + }, + }; + + // Minimal typed fixture: the client forwards txRequest opaquely. + const txRequestFixture: TxRequest = { + txRequestId: '123456', + walletId: '68489ecff6fb16304670b327db8eb31a', + walletType: 'advanced', + version: 1, + state: 'initialized', + date: new Date().toISOString(), + userId: 'test-user', + intent: {}, + policiesChecked: true, + latest: true, + unsignedTxs: [], + transactions: [ + { + state: 'initialized', + unsignedTx: { + serializedTxHex: 'deadbeef', + signableHex: 'testMessage', + derivationPath: 'm/0', + }, + signatureShares: [], + }, + ], + }; + + const mockRound1Response = { + signatureShareRound1: { from: 'user', to: 'bitgo', payload: 'mock-round1-payload' }, + userGpgPubKey: 'mock-user-gpg-pub-key', + encryptedRound1Session: 'mock-encrypted-round1-session', + encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', + encryptedDataKey: 'mock-encrypted-data-key', + }; + + afterEach(() => { + nock.cleanAll(); + }); + + it('should include curveType in the round 1 request body when provided', async () => { + const client = new AdvancedWalletManagerClient(cfg, coin); + + const nockScope = nock(awmUrl) + .post('/api/tsol/mpc/sign/mpcv2round1', (body) => { + body.should.have.property('curveType', 'eddsa'); + return true; + }) + .reply(200, mockRound1Response); + + const response = await client.signMPCv2Round1('user', 'mock-pub', { + txRequest: txRequestFixture, + curveType: 'eddsa', + }); + + response.should.have.property('signatureShareRound1'); + nockScope.done(); + }); + + it('should omit curveType from the round 1 request body when not provided', async () => { + const client = new AdvancedWalletManagerClient(cfg, coin); + + const nockScope = nock(awmUrl) + .post('/api/tsol/mpc/sign/mpcv2round1', (body) => { + body.should.not.have.property('curveType'); + return true; + }) + .reply(200, mockRound1Response); + + const response = await client.signMPCv2Round1('user', 'mock-pub', { + txRequest: txRequestFixture, + }); + + response.should.have.property('signatureShareRound1'); + nockScope.done(); + }); + + it('should include curveType in the round 2 request body when provided', async () => { + const client = new AdvancedWalletManagerClient(cfg, coin); + + const mockRound2Response = { + signatureShareRound2: { from: 'user', to: 'bitgo', payload: 'mock-round2-payload' }, + encryptedRound2Session: 'mock-encrypted-round2-session', + }; + + const nockScope = nock(awmUrl) + .post('/api/tsol/mpc/sign/mpcv2round2', (body) => { + body.should.have.property('curveType', 'eddsa'); + return true; + }) + .reply(200, mockRound2Response); + + const response = await client.signMPCv2Round2('user', 'mock-pub', { + txRequest: txRequestFixture, + encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', + encryptedRound1Session: 'mock-encrypted-round1-session', + encryptedDataKey: 'mock-encrypted-data-key', + bitgoPublicGpgKey: 'mock-bitgo-gpg-key', + curveType: 'eddsa', + }); + + response.should.have.property('signatureShareRound2'); + nockScope.done(); + }); + + it('should include curveType in the round 3 request body when provided', async () => { + const client = new AdvancedWalletManagerClient(cfg, coin); + + const mockRound3Response = { + signatureShareRound3: { from: 'user', to: 'bitgo', payload: 'mock-round3-payload' }, + }; + + const nockScope = nock(awmUrl) + .post('/api/tsol/mpc/sign/mpcv2round3', (body) => { + body.should.have.property('curveType', 'eddsa'); + return true; + }) + .reply(200, mockRound3Response); + + const response = await client.signMPCv2Round3('user', 'mock-pub', { + txRequest: txRequestFixture, + encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', + encryptedRound2Session: 'mock-encrypted-round2-session', + encryptedDataKey: 'mock-encrypted-data-key', + bitgoPublicGpgKey: 'mock-bitgo-gpg-key', + curveType: 'eddsa', + }); + + response.should.have.property('signatureShareRound3'); + nockScope.done(); + }); +}); diff --git a/src/advancedWalletManager/routers/advancedWalletManagerApiSpec.ts b/src/advancedWalletManager/routers/advancedWalletManagerApiSpec.ts index 958ce036..19a670ac 100644 --- a/src/advancedWalletManager/routers/advancedWalletManagerApiSpec.ts +++ b/src/advancedWalletManager/routers/advancedWalletManagerApiSpec.ts @@ -126,7 +126,10 @@ const SignMpcRequest = { bitgoPublicGpgKey: t.union([t.undefined, t.string]), encryptedDataKey: t.union([t.undefined, t.string]), - // ECDSA MPCv2 specific fields + // Curve type hint for MPCv2 signing (optional, backward compatible) + curveType: t.union([t.undefined, t.literal('eddsa'), t.literal('ecdsa')]), + + // MPCv2 specific fields (ECDSA and EdDSA) encryptedUserGpgPrvKey: t.union([t.undefined, t.string]), encryptedRound1Session: t.union([t.undefined, t.string]), encryptedRound2Session: t.union([t.undefined, t.string]), diff --git a/src/masterBitgoExpress/clients/advancedWalletManagerClient.ts b/src/masterBitgoExpress/clients/advancedWalletManagerClient.ts index a43100d5..fc3b28f6 100644 --- a/src/masterBitgoExpress/clients/advancedWalletManagerClient.ts +++ b/src/masterBitgoExpress/clients/advancedWalletManagerClient.ts @@ -150,9 +150,10 @@ interface SignMpcGShareResponse { gShare: GShare; } -// ECDSA MPCv2 interfaces +// MPCv2 interfaces (ECDSA and EdDSA) interface SignMpcV2Round1Params { txRequest: TxRequest; + curveType?: 'eddsa' | 'ecdsa'; } export interface SignMpcV2Round1Response { @@ -169,6 +170,7 @@ interface SignMpcV2Round2Params { encryptedRound1Session: string; encryptedDataKey: string; bitgoPublicGpgKey: string; + curveType?: 'eddsa' | 'ecdsa'; } export interface SignMpcV2Round2Response { @@ -182,6 +184,7 @@ interface SignMpcV2Round3Params { encryptedRound2Session: string; encryptedDataKey: string; bitgoPublicGpgKey: string; + curveType?: 'eddsa' | 'ecdsa'; } export interface SignMpcV2Round3Response {