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
194 changes: 183 additions & 11 deletions src/__tests__/api/master/eddsaMpcv2Signing.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
import 'should';

import nock from 'nock';
import * as sinon from 'sinon';
import { TxRequest } from '@bitgo-beta/sdk-core';
import {
Environments,
IRequestTracer,
openpgpUtils,
RequestTracer,
Wallet,
} from '@bitgo-beta/sdk-core';
import { BitGoAPI } from '@bitgo-beta/sdk-api';
import {
AdvancedWalletManagerClient,
SignMpcV2Round1Response,
SignMpcV2Round2Response,
} from '../../../masterBitgoExpress/clients/advancedWalletManagerClient';
import { createEddsaMPCv2CustomSigners } from '../../../masterBitgoExpress/handlers/eddsa';
import {
createEddsaMPCv2CustomSigners,
signAndSendEddsaMPCv2FromTxRequest,
} from '../../../masterBitgoExpress/handlers/eddsa';
import { createMPCSendParamsWithCustomSigningFns } from '../../../masterBitgoExpress/handlers/handleSendMany';
import coinFactory from '../../../shared/coinFactory';
import {
BitGoAPITestHarness,
buildEcdsaMpcv2TxRequest,
nockEcdsaMpcv2SigningFlow,
} from './testUtils';

describe('createEddsaMPCv2CustomSigners', () => {
const source = 'user' as const;
Expand Down Expand Up @@ -96,8 +116,8 @@ describe('createEddsaMPCv2CustomSigners', () => {
const response = await customEddsaMPCv2Round2Generator({
txRequest: txRequestFixture,
bitgoPublicGpgKey: 'mock-bitgo-gpg-key',
encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key',
encryptedRound1Session: 'mock-encrypted-round1-session',
encryptedUserGpgPrvKey: 'caller-encrypted-user-gpg-prv-key',
encryptedRound1Session: 'caller-encrypted-round1-session',
});

response.should.equal(mockRound2Response);
Expand Down Expand Up @@ -130,14 +150,14 @@ describe('createEddsaMPCv2CustomSigners', () => {
await customEddsaMPCv2Round2Generator({
txRequest: txRequestFixture,
bitgoPublicGpgKey: 'mock-bitgo-gpg-key',
encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key',
encryptedRound1Session: 'mock-encrypted-round1-session',
encryptedUserGpgPrvKey: 'caller-encrypted-user-gpg-prv-key',
encryptedRound1Session: 'caller-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',
encryptedUserGpgPrvKey: 'caller-encrypted-user-gpg-prv-key',
encryptedRound2Session: 'caller-encrypted-round2-session',
});

response.should.equal(mockRound3Response);
Expand Down Expand Up @@ -168,8 +188,8 @@ describe('createEddsaMPCv2CustomSigners', () => {
await customEddsaMPCv2Round2Generator({
txRequest: txRequestFixture,
bitgoPublicGpgKey: 'mock-bitgo-gpg-key',
encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key',
encryptedRound1Session: 'mock-encrypted-round1-session',
encryptedUserGpgPrvKey: 'caller-encrypted-user-gpg-prv-key',
encryptedRound1Session: 'caller-encrypted-round1-session',
}).should.be.rejectedWith('Round 1 must be completed before Round 2');
awmCalls.should.have.length(0);
});
Expand All @@ -182,9 +202,161 @@ describe('createEddsaMPCv2CustomSigners', () => {
await customEddsaMPCv2Round3Generator({
txRequest: txRequestFixture,
bitgoPublicGpgKey: 'mock-bitgo-gpg-key',
encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key',
encryptedRound2Session: 'mock-encrypted-round2-session',
encryptedUserGpgPrvKey: 'caller-encrypted-user-gpg-prv-key',
encryptedRound2Session: 'caller-encrypted-round2-session',
}).should.be.rejectedWith('Round 2 must be completed before Round 3');
awmCalls.should.have.length(1);
});
});
describe('signAndSendEddsaMPCv2FromTxRequest', () => {
let bitgo: BitGoAPI;
let wallet: Wallet;
let awmClient: AdvancedWalletManagerClient;
let reqId: IRequestTracer;
const bitgoApiUrl = Environments.local.uri;
const advancedWalletManagerUrl = 'http://advancedwalletmanager.invalid';
const coin = 'tsol'; // Use tsol for EdDSA testing

before(() => {
nock.disableNetConnect();
});

beforeEach(() => {
bitgo = new BitGoAPI({ env: 'local' });
wallet = {
id: () => 'test-wallet-id',
baseCoin: {
getMPCAlgorithm: () => 'eddsa',
},
multisigTypeVersion: () => 'MPCv2',
} as unknown as Wallet;
awmClient = new AdvancedWalletManagerClient(
{
advancedWalletManagerUrl,
awmServerCaCert: 'dummy-cert',
tlsMode: 'disabled',
clientCertAllowSelfSigned: true,
} as unknown as ConstructorParameters<typeof AdvancedWalletManagerClient>[0],
coin,
);
reqId = new RequestTracer();
});

afterEach(() => {
nock.cleanAll();
sinon.restore();
BitGoAPITestHarness.clearConstantsCache();
});

after(() => {
nock.enableNetConnect();
});

it('should successfully sign an EdDSA MPCv2 transaction', async () => {
const txRequestData = buildEcdsaMpcv2TxRequest('pendingUserSignature');
const txRequest = txRequestData as TxRequest;

const bitgoEddsaMpcv2Key = await openpgpUtils.generateGPGKeyPair('ed25519');

nock(bitgoApiUrl)
.persist()
.get('/api/v1/client/constants')
.reply(200, {
constants: {
mpc: {
bitgoEddsaMpcv2PublicKey: bitgoEddsaMpcv2Key.publicKey,
bitgoPublicKey: bitgoEddsaMpcv2Key.publicKey,
},
},
});

const sendResponse = { ...txRequestData, state: 'signed' };
const nocks = nockEcdsaMpcv2SigningFlow({
coin,
bitgoApiUrl,
advancedWalletManagerUrl,
sendResponse,
expectedCurveType: 'eddsa',
});

const result = await signAndSendEddsaMPCv2FromTxRequest(
bitgo,
wallet,
txRequest,
awmClient,
'user',
'mock-common-keychain',
reqId,
);

result.should.eql(sendResponse);

nocks.round1SignNock.done();
nocks.round2SignNock.done();
nocks.round3SignNock.done();
nocks.sendTxNock.done();
nocks.awmRound1Nock.done();
nocks.awmRound2Nock.done();
nocks.awmRound3Nock.done();
});
});

describe('createMPCSendParamsWithCustomSigningFns EdDSA dispatch', () => {
const coin = 'tsol';

function makeReq(decoded: Record<string, unknown>) {
return {
params: { coin },
decoded,
bitgo: {} as never,
} as unknown as Parameters<typeof createMPCSendParamsWithCustomSigningFns>[0];
}

afterEach(() => {
sinon.restore();
});

it('returns MPCv2 signing functions for an EdDSA MPCv2 wallet', async () => {
sinon.stub(coinFactory, 'getCoin').resolves({
getMPCAlgorithm: () => 'eddsa',
} as never);
const wallet = { multisigTypeVersion: () => 'MPCv2' } as unknown as Wallet;
const awmClientStub = {} as unknown as AdvancedWalletManagerClient;

const params = await createMPCSendParamsWithCustomSigningFns(
makeReq({ recipients: [] }),
wallet,
awmClientStub,
{ source: 'user', commonKeychain: 'mock-common-keychain' } as never,
);

params.should.have.property('customEddsaMPCv2SigningRound1GenerationFunction');
params.should.have.property('customEddsaMPCv2SigningRound2GenerationFunction');
params.should.have.property('customEddsaMPCv2SigningRound3GenerationFunction');
params.should.not.have.property('customCommitmentGeneratingFunction');
params.should.not.have.property('customRShareGeneratingFunction');
params.should.not.have.property('customGShareGeneratingFunction');
});

it('returns MPCv1 signing functions for an EdDSA MPCv1 wallet', async () => {
sinon.stub(coinFactory, 'getCoin').resolves({
getMPCAlgorithm: () => 'eddsa',
} as never);
const wallet = {} as unknown as Wallet;
const awmClientStub = {} as unknown as AdvancedWalletManagerClient;

const params = await createMPCSendParamsWithCustomSigningFns(
makeReq({ recipients: [] }),
wallet,
awmClientStub,
{ source: 'user', commonKeychain: 'mock-common-keychain' } as never,
);

params.should.have.property('customCommitmentGeneratingFunction');
params.should.have.property('customRShareGeneratingFunction');
params.should.have.property('customGShareGeneratingFunction');
params.should.not.have.property('customEddsaMPCv2SigningRound1GenerationFunction');
params.should.not.have.property('customEddsaMPCv2SigningRound2GenerationFunction');
params.should.not.have.property('customEddsaMPCv2SigningRound3GenerationFunction');
});
});
76 changes: 76 additions & 0 deletions src/__tests__/api/master/signAndSendTxRequest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -331,4 +331,80 @@ describe('POST /api/v1/:coin/advancedwallet/:walletId/txrequest/:txRequestId/sig
signMpcGShareNockAwm.done();
});
});

describe('EdDSA MPCv2 Sign and Send:', () => {
const eddsaCoin = 'tsol'; // Use tsol for EdDSA testing

it('should successfully sign and send EdDSA MPCv2 transaction with user key', async () => {
const walletNock = nock(bitgoApiUrl)
.get(`/api/v2/${eddsaCoin}/wallet/${walletId}`)
.matchHeader('any', () => true)
.reply(200, {
id: walletId,
type: 'advanced',
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
multisigType: 'tss',
multisigTypeVersion: 'MPCv2',
coin: eddsaCoin,
});

const keychainNock = nock(bitgoApiUrl)
.get(`/api/v2/${eddsaCoin}/key/user-key-id`)
.matchHeader('any', () => true)
.reply(200, {
id: 'user-key-id',
pub: 'xpub_user',
commonKeychain: 'common-keychain-123',
source: 'user',
});

const getTxRequestNock = nock(bitgoApiUrl)
.get(`/api/v2/wallet/${walletId}/txrequests`)
.query({ txRequestIds: txRequestId, latest: true })
.matchHeader('any', () => true)
.reply(200, { txRequests: [buildEcdsaMpcv2TxRequest('pendingUserSignature')] });

const signedTxRequest = buildEcdsaMpcv2TxRequest('signed', {
extra: {
transactions: [
{
unsignedTx: { derivationPath: 'm/0', signableHex: 'testMessage' },
state: 'signed',
signedTx: { id: 'test-tx-id', tx: 'signed-transaction-hex' },
},
],
},
});
const nocks = nockEcdsaMpcv2SigningFlow({
coin: eddsaCoin,
bitgoApiUrl,
advancedWalletManagerUrl,
sendResponse: signedTxRequest,
includeBitgoKeychainNock: true,
expectedCurveType: 'eddsa',
});

const response = await agent
.post(
`/api/v1/${eddsaCoin}/advancedwallet/${walletId}/txrequest/${txRequestId}/signAndSend`,
)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'user', commonKeychain: 'common-keychain-123' });

response.status.should.equal(200);
response.body.should.have.property('txid', 'test-tx-id');
response.body.should.have.property('tx', 'signed-transaction-hex');

walletNock.done();
keychainNock.done();
getTxRequestNock.done();
nocks.round1SignNock.done();
nocks.round2SignNock.done();
nocks.round3SignNock.done();
nocks.sendTxNock.done();
nocks.awmRound1Nock.done();
nocks.awmRound2Nock.done();
nocks.awmRound3Nock.done();
});
});
});
11 changes: 8 additions & 3 deletions src/__tests__/api/master/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,8 @@ export interface NockEcdsaMpcv2SigningFlowOptions {
includeBitgoKeychainNock?: boolean;
/** Base tx request for BitGo sign round replies. */
pendingTxRequest?: ReturnType<typeof buildEcdsaMpcv2TxRequest>;
/** When set, the AWM round nocks only match request bodies carrying this curveType. */
expectedCurveType?: 'eddsa' | 'ecdsa';
}

/**
Expand All @@ -328,6 +330,9 @@ export function nockEcdsaMpcv2SigningFlow(options: NockEcdsaMpcv2SigningFlowOpti
const txRequestId = options.txRequestId ?? DEFAULT_ECDSA_MPCV2_TX_REQUEST_ID;
const userGpgPubKey = options.userGpgPubKey ?? 'user-gpg-pub-key';
const commonKeychain = options.commonKeychain ?? 'common-keychain-123';
const curveTypeFilter = options.expectedCurveType
? (body: Record<string, unknown>) => body.curveType === options.expectedCurveType
: undefined;

const { round1SignatureShare, round2SignatureShare, round3SignatureShare } =
createEcdsaMpcv2SignatureShares();
Expand Down Expand Up @@ -390,7 +395,7 @@ export function nockEcdsaMpcv2SigningFlow(options: NockEcdsaMpcv2SigningFlowOpti
.reply(200, options.sendResponse);

const awmRound1Nock = nock(options.advancedWalletManagerUrl)
.post(`/api/${options.coin}/mpc/sign/mpcv2round1`)
.post(`/api/${options.coin}/mpc/sign/mpcv2round1`, curveTypeFilter)
.reply(200, {
signatureShareRound1: round1SignatureShare,
userGpgPubKey,
Expand All @@ -400,14 +405,14 @@ export function nockEcdsaMpcv2SigningFlow(options: NockEcdsaMpcv2SigningFlowOpti
});

const awmRound2Nock = nock(options.advancedWalletManagerUrl)
.post(`/api/${options.coin}/mpc/sign/mpcv2round2`)
.post(`/api/${options.coin}/mpc/sign/mpcv2round2`, curveTypeFilter)
.reply(200, {
signatureShareRound2: round2SignatureShare,
encryptedRound2Session: 'encrypted-round2-session',
});

const awmRound3Nock = nock(options.advancedWalletManagerUrl)
.post(`/api/${options.coin}/mpc/sign/mpcv2round3`)
.post(`/api/${options.coin}/mpc/sign/mpcv2round3`, curveTypeFilter)
.reply(200, { signatureShareRound3: round3SignatureShare });

return {
Expand Down
Loading
Loading