Skip to content
Open
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
33 changes: 30 additions & 3 deletions modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EncryptionVersion, IRequestTracer } from '../../../api';
import * as openpgp from 'openpgp';
import { Key, readKey, SerializedKeyPair } from 'openpgp';
import { IBaseCoin, KeychainsTriplet } from '../../baseCoin';
import { IBaseCoin, KeychainsTriplet, TransactionParams } from '../../baseCoin';
import { BitGoBase } from '../../bitgoBase';
import { Keychain, KeyIndices, WebauthnKeyEncryptionInfo } from '../../keychain';
import { getTxRequest } from '../../tss';
Expand Down Expand Up @@ -31,6 +31,7 @@ import {
IntentOptionsForMessage,
IntentOptionsForTypedData,
ITssUtils,
PopulatedIntent,
PopulatedIntentForMessageSigning,
PopulatedIntentForTypedDataSigning,
PrebuildTransactionWithIntentOptions,
Expand All @@ -50,6 +51,25 @@ import { getBitgoGpgPubKey } from '../opengpgUtils';
import assert from 'assert';
import { MessageStandardType } from '../messageTypes';

/**
* Derives txParams from the persisted intent on a TxRequest for EdDSA MPCv2 signing paths
* where no SDK-local buildParams is available (PA path and UI re-sign path).
* Native coin transfers (where symbol equals the chain name) are excluded from tokenName.
*/
export function txParamsFromIntent(intent: unknown, chainName: string): TransactionParams | undefined {
const intentRecipients = (intent as PopulatedIntent | undefined)?.recipients;
if (!intentRecipients?.length) {
return undefined;
}
Comment on lines +60 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can this be accessed via type narrowing instead of casting?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

also what if an intent is valid for having no recipients wouldn't this throw downstream during verification?

return {
recipients: intentRecipients.map((r) => ({
address: r.address.address,
amount: String(r.amount.value),
...(r.amount.symbol && r.amount.symbol !== chainName && { tokenName: r.amount.symbol }),
})),
};
}

/**
* BaseTssUtil class which different signature schemes have to extend
*/
Expand Down Expand Up @@ -579,8 +599,15 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
async recreateTxRequest(txRequestId: string, decryptedPrv: string, reqId: IRequestTracer): Promise<TxRequest> {
Comment thread
danielpeng1 marked this conversation as resolved.
await this.deleteSignatureShares(txRequestId, reqId);
// after delete signatures shares get the tx without them
const txRequest = await getTxRequest(this.bitgo, this.wallet.id(), txRequestId, reqId);
return await this.signTxRequest({ txRequest, prv: decryptedPrv, reqId });
const txRequest = await this.getTxRequest(txRequestId, reqId);
// EdDSA MPCv2 re-verifies the transaction against txParams.recipients before DSG starts.
// On the PA path there is no SDK-local buildParams, so derive txParams from the persisted
// intent. Other TSS variants either skip recipient verification or already work without txParams.
const txParams =
this.wallet.multisigTypeVersion() === 'MPCv2' && this.baseCoin.getMPCAlgorithm() === 'eddsa'
? txParamsFromIntent(txRequest.intent, this.baseCoin.getChain())
: undefined;
return await this.signTxRequest({ txRequest, prv: decryptedPrv, reqId, txParams });
}

/**
Expand Down
22 changes: 20 additions & 2 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SignedMessage,
SignedTransaction,
SignedTransactionRequest,
TransactionParams,
TransactionPrebuild,
VerifyAddressOptions,
} from '../baseCoin';
Expand Down Expand Up @@ -57,6 +58,7 @@ import {
TxRequest,
} from '../utils';
import { postWithCodec } from '../utils/postWithCodec';
import { txParamsFromIntent } from '../utils/tss/baseTSSUtils';
import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa';
import EddsaUtils, { EddsaMPCv2Utils } from '../utils/tss/eddsa';
import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest';
Expand Down Expand Up @@ -4816,9 +4818,25 @@ export class Wallet implements IWallet {
}

try {
let txRequest: string | TxRequest = params.txPrebuild.txRequestId;
let txParams: TransactionParams | undefined = params.txPrebuild.buildParams;

// EdDSA MPCv2 re-sign path: buildParams is absent when the UI calls signAndSendTxRequest with
// only txRequestId. Derive txParams from the persisted intent so verifyTransaction receives
// the correct recipients before DSG starts. Other TSS variants are unaffected by the guard.
if (!txParams && this.multisigTypeVersion() === 'MPCv2' && this.baseCoin.getMPCAlgorithm() === 'eddsa') {
txRequest = await getTxRequest(
this.bitgo,
this.id(),
params.txPrebuild.txRequestId,
params.reqId || new RequestTracer()
);
txParams = txParamsFromIntent(txRequest.intent, this.baseCoin.getChain());
}

return await this.tssUtils!.signTxRequest({
txRequest: params.txPrebuild.txRequestId,
txParams: params.txPrebuild.buildParams,
txRequest,
txParams,
prv: params.prv,
reqId: params.reqId || new RequestTracer(),
apiVersion: params.apiVersion,
Expand Down
103 changes: 102 additions & 1 deletion modules/sdk-core/test/unit/bitgo/utils/tss/baseTSSUtils.ts
Comment thread
danielpeng1 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as sinon from 'sinon';
import * as sjcl from '@bitgo/sjcl';
import { DklsUtils } from '@bitgo/sdk-lib-mpc';

import { BitGoBase, EcdsaMPCv2Utils, IBaseCoin, TxRequest } from '../../../../../src';
import { BitGoBase, EcdsaMPCv2Utils, IBaseCoin, IWallet, RequestTracer, TxRequest } from '../../../../../src';
import BaseTssUtils from '../../../../../src/bitgo/utils/tss/baseTSSUtils';

type BitgoGpgKeyPair = openpgp.SerializedKeyPair<string> & { revocationCertificate: string };
Expand Down Expand Up @@ -322,6 +322,107 @@ describe('Base TSS Utils', function () {
});
});

describe('recreateTxRequest', function () {
function makeWallet(multisigTypeVersion: 'MPCv2' | undefined): IWallet {
return {
id: sinon.stub().returns('wallet-id'),
multisigTypeVersion: sinon.stub().returns(multisigTypeVersion),
} as unknown as IWallet;
}

function makeCoin(mpcAlgorithm: 'eddsa' | 'ecdsa', chain = 'tsol'): IBaseCoin {
const coin = {} as IBaseCoin;
coin.getHashFunction = sinon.stub();
coin.getMPCAlgorithm = sinon.stub().returns(mpcAlgorithm);
coin.getChain = sinon.stub().returns(chain);
return coin;
}

it('derives txParams from intent for EdDSA MPCv2 wallets', async function () {
const txRequestId = 'tx-req-id-1';
const reqId = new RequestTracer();
const txRequest = buildTxRequest({
txRequestId,
intent: {
intentType: 'payment',
recipients: [{ address: { address: 'solAddr1' }, amount: { value: '5000000', symbol: 'tsol' } }],
},
});

const utils = new TestBaseTssUtils(mockBitgo, makeCoin('eddsa'), makeWallet('MPCv2'));
sinon.stub(utils, 'deleteSignatureShares').resolves();
sinon.stub(utils, 'getTxRequest').resolves(txRequest);
const signTxRequestStub = sinon.stub(utils, 'signTxRequest').resolves(txRequest);

await utils.recreateTxRequest(txRequestId, 'prv', reqId);

// Native SOL: symbol equals the chain name, so tokenName must be omitted
assert.deepStrictEqual(signTxRequestStub.firstCall.args[0].txParams, {
recipients: [{ address: 'solAddr1', amount: '5000000' }],
});
});

it('sets tokenName for SPL tokens (symbol differs from chain name)', async function () {
const txRequestId = 'tx-req-id-spl';
const reqId = new RequestTracer();
const txRequest = buildTxRequest({
txRequestId,
intent: {
intentType: 'payment',
recipients: [{ address: { address: 'splAddr1' }, amount: { value: '1000', symbol: 'tsol:usdc' } }],
},
});

const utils = new TestBaseTssUtils(mockBitgo, makeCoin('eddsa', 'tsol'), makeWallet('MPCv2'));
sinon.stub(utils, 'deleteSignatureShares').resolves();
sinon.stub(utils, 'getTxRequest').resolves(txRequest);
const signTxRequestStub = sinon.stub(utils, 'signTxRequest').resolves(txRequest);

await utils.recreateTxRequest(txRequestId, 'prv', reqId);

// SPL token: symbol differs from chain name, so tokenName must be set
assert.deepStrictEqual(signTxRequestStub.firstCall.args[0].txParams, {
recipients: [{ address: 'splAddr1', amount: '1000', tokenName: 'tsol:usdc' }],
});
});

it('passes undefined txParams for EdDSA MPCv2 when intent has no recipients', async function () {
const txRequestId = 'tx-req-id-2';
const reqId = new RequestTracer();
const txRequest = buildTxRequest({ txRequestId, intent: { intentType: 'enableToken' } });

const utils = new TestBaseTssUtils(mockBitgo, makeCoin('eddsa'), makeWallet('MPCv2'));
sinon.stub(utils, 'deleteSignatureShares').resolves();
sinon.stub(utils, 'getTxRequest').resolves(txRequest);
const signTxRequestStub = sinon.stub(utils, 'signTxRequest').resolves(txRequest);

await utils.recreateTxRequest(txRequestId, 'prv', reqId);

assert.strictEqual(signTxRequestStub.firstCall.args[0].txParams, undefined);
});

it('passes undefined txParams for ECDSA MPCv2 wallets (guard does not apply)', async function () {
const txRequestId = 'tx-req-id-3';
const reqId = new RequestTracer();
const txRequest = buildTxRequest({
txRequestId,
intent: {
intentType: 'payment',
recipients: [{ address: { address: 'ethAddr1' }, amount: { value: '1000000', symbol: 'eth' } }],
},
});

const utils = new TestBaseTssUtils(mockBitgo, makeCoin('ecdsa'), makeWallet('MPCv2'));
sinon.stub(utils, 'deleteSignatureShares').resolves();
sinon.stub(utils, 'getTxRequest').resolves(txRequest);
const signTxRequestStub = sinon.stub(utils, 'signTxRequest').resolves(txRequest);

await utils.recreateTxRequest(txRequestId, 'prv', reqId);

assert.strictEqual(signTxRequestStub.firstCall.args[0].txParams, undefined);
});
});

describe('ECDSA MPC v2 delegated txRequest parsing', function () {
it('getHashStringAndDerivationPath produces the correct hashBuffer and derivationPath', async function () {
const signableHex = 'deadbeef';
Expand Down
Loading