Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 107 additions & 1 deletion src/__tests__/api/advancedWalletManager/signMpcTransaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Comment thread
vibhavgo marked this conversation as resolved.
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');
});
});
});
159 changes: 159 additions & 0 deletions src/__tests__/api/master/advancedWalletManagerClient.test.ts
Comment thread
vibhavgo marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -169,6 +170,7 @@ interface SignMpcV2Round2Params {
encryptedRound1Session: string;
encryptedDataKey: string;
bitgoPublicGpgKey: string;
curveType?: 'eddsa' | 'ecdsa';
}

export interface SignMpcV2Round2Response {
Expand All @@ -182,6 +184,7 @@ interface SignMpcV2Round3Params {
encryptedRound2Session: string;
encryptedDataKey: string;
bitgoPublicGpgKey: string;
curveType?: 'eddsa' | 'ecdsa';
}

export interface SignMpcV2Round3Response {
Expand Down
Loading