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
27 changes: 1 addition & 26 deletions src/__tests__/api/master/eddsa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -266,7 +242,6 @@ xdescribe('Eddsa Signing Handler', () => {
state: 'signed',
});

getTxRequestNock.done();
exchangeCommitmentsNock.done();
offerRShareNock.done();
getBitgoRShareNock.done();
Expand Down
78 changes: 25 additions & 53 deletions src/__tests__/api/master/sendMany.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}`)
Expand All @@ -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}`)
Expand All @@ -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);
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/api/master/clients/enclavedExpressClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ interface SignMpcCommitmentParams {
pub: string;
}

interface SignMpcCommitmentResponse {
export interface SignMpcCommitmentResponse {
userToBitgoCommitment: CommitmentShareRecord;
encryptedSignerShare: EncryptedSignerShareRecord;
encryptedUserToBitgoRShare: EncryptedSignerShareRecord;
Expand Down
159 changes: 91 additions & 68 deletions src/api/master/handlers/eddsa.ts
Original file line number Diff line number Diff line change
@@ -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;

Copilot AI Jul 10, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commitmentResponse variable is declared but not initialized, which could lead to runtime errors if customRShareGenerator or customGShareGenerator are called before customCommitmentGenerator. Consider initializing with undefined or adding explicit undefined checks.

Suggested change
let commitmentResponse: SignMpcCommitmentResponse;
let commitmentResponse: SignMpcCommitmentResponse | undefined = undefined;

Copilot uses AI. Check for mistakes.

// 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,
Expand All @@ -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 {
Expand Down
Loading