Skip to content

Commit 063b097

Browse files
feat(sdk-core): add explicit recipientSource typing for TSS signTxRequest
Introduce TssTxRecipientSource and TssSignTxRequestParams union so callers can opt into compile-time enforcement of non-empty txParams.recipients via recipientSource: Explicit. Default (resolved) matches existing optional txParams behavior. ECDSA signing validates explicit mode at runtime for JS callers. ITssUtils.signTxRequest now references the stricter param type. Add MPCv2 unit test covering the explicit branch. Refs: WAL-375 #8462 WAL-375
1 parent 430c661 commit 063b097

6 files changed

Lines changed: 118 additions & 17 deletions

File tree

modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/signTxRequest.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
RequestTracer,
88
SignatureShareRecord,
99
SignatureShareType,
10+
TssTxRecipientSource,
1011
TxRequest,
1112
Wallet,
1213
} from '@bitgo/sdk-core';
@@ -199,6 +200,31 @@ describe('signTxRequest:', function () {
199200
nockPromises[2].isDone().should.be.true();
200201
});
201202

203+
it('successfully signs when recipientSource is explicit and txParams.recipients is non-empty', async function () {
204+
const nockPromises = [
205+
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey),
206+
await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequest, bitgoGpgKey),
207+
await nockTxRequestResponseSignatureShareRoundThree(txRequest),
208+
await nockSendTxRequest(txRequest),
209+
];
210+
await Promise.all(nockPromises);
211+
212+
const userShare = fs.readFileSync(shareFiles[vector.party1]);
213+
const userPrvBase64 = Buffer.from(userShare).toString('base64');
214+
await tssUtils.signTxRequest({
215+
txRequest,
216+
prv: userPrvBase64,
217+
reqId,
218+
recipientSource: TssTxRecipientSource.Explicit,
219+
txParams: {
220+
recipients: [{ address: '0x0000000000000000000000000000000000000001', amount: '1' }],
221+
},
222+
});
223+
nockPromises[0].isDone().should.be.true();
224+
nockPromises[1].isDone().should.be.true();
225+
nockPromises[2].isDone().should.be.true();
226+
});
227+
202228
it('successfully signs a txRequest with backup key for a dkls hot wallet with WP', async function () {
203229
const nockPromises = [
204230
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey, 1),

modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import {
3535
SignatureShareRecord,
3636
TSSParams,
3737
TSSParamsForMessage,
38-
TSSParamsWithPrv,
38+
TssSignTxRequestParamsWithPrv,
3939
TxRequest,
4040
TxRequestVersion,
4141
} from './baseTypes';
@@ -198,7 +198,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
198198
throw new Error('Method not implemented.');
199199
}
200200

201-
signTxRequest(params: TSSParamsWithPrv): Promise<TxRequest> {
201+
signTxRequest(params: TssSignTxRequestParamsWithPrv): Promise<TxRequest> {
202202
throw new Error('Method not implemented.');
203203
}
204204

modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Key, SerializedKeyPair } from 'openpgp';
22
import { IRequestTracer } from '../../../api';
3-
import { KeychainsTriplet, ParsedTransaction, TransactionParams } from '../../baseCoin';
3+
import { type ITransactionRecipient, KeychainsTriplet, ParsedTransaction, TransactionParams } from '../../baseCoin';
44
import { ApiKeyShare, Keychain } from '../../keychain';
55
import { ApiVersion, Memo, WalletType } from '../../wallet';
66
import { EDDSA, GShare, Signature, SignShare } from '../../../account-lib/mpc/tss';
@@ -532,16 +532,6 @@ export interface EncryptedSignerShareRecord extends ShareBaseRecord {
532532
type: EncryptedSignerShareType;
533533
}
534534

535-
export type TSSParamsWithPrv = TSSParams & {
536-
prv: string;
537-
mpcv2PartyId?: 0 | 1;
538-
};
539-
540-
export type TSSParamsForMessageWithPrv = TSSParamsForMessage & {
541-
prv: string;
542-
mpcv2PartyId?: 0 | 1;
543-
};
544-
545535
export type BitgoPubKeyType = 'nitro' | 'onprem';
546536

547537
export type TSSParams = {
@@ -557,6 +547,60 @@ export type TSSParamsForMessage = TSSParams & {
557547
bufferToSign: Buffer;
558548
};
559549

550+
/** At least one recipient (when using `recipientSource: TssTxRecipientSource.Explicit`). */
551+
export type NonEmptyRecipientList = [ITransactionRecipient, ...ITransactionRecipient[]];
552+
553+
/** txParams including a non-empty recipients list for strict signing verification typing. */
554+
export type TransactionParamsWithMandatoryRecipients = TransactionParams & {
555+
recipients: NonEmptyRecipientList;
556+
};
557+
558+
export const TssTxRecipientSource = {
559+
/** Require txParams.recipients with at least one entry (enforced by TypeScript for this branch). */
560+
Explicit: 'explicit',
561+
/**
562+
* Default: txParams may be omitted or partial; verification uses coin-specific rules
563+
* (for example recipients from txRequest context).
564+
*/
565+
Resolved: 'resolved',
566+
} as const;
567+
568+
export type TssTxRecipientSource = (typeof TssTxRecipientSource)[keyof typeof TssTxRecipientSource];
569+
570+
export type TssSignTxExplicitRecipientParams = {
571+
txRequest: string | TxRequest;
572+
reqId: IRequestTracer;
573+
apiVersion?: ApiVersion;
574+
recipientSource: typeof TssTxRecipientSource.Explicit;
575+
txParams: TransactionParamsWithMandatoryRecipients;
576+
};
577+
578+
export type TssSignTxResolvedRecipientParams = {
579+
txRequest: string | TxRequest;
580+
reqId: IRequestTracer;
581+
apiVersion?: ApiVersion;
582+
recipientSource?: typeof TssTxRecipientSource.Resolved;
583+
txParams?: TransactionParams;
584+
};
585+
586+
/**
587+
* Parameters for TSS transaction signing ({@link ITssUtils.signTxRequest}).
588+
* Set {@link TssTxRecipientSource.Explicit} to require a non-empty txParams.recipients array at compile time.
589+
*/
590+
export type TssSignTxRequestParams = TssSignTxExplicitRecipientParams | TssSignTxResolvedRecipientParams;
591+
592+
export type TssSignTxRequestParamsWithPrv = TssSignTxRequestParams & {
593+
prv: string;
594+
mpcv2PartyId?: 0 | 1;
595+
};
596+
597+
export type TSSParamsWithPrv = TssSignTxRequestParamsWithPrv;
598+
599+
export type TSSParamsForMessageWithPrv = TSSParamsForMessage & {
600+
prv: string;
601+
mpcv2PartyId?: 0 | 1;
602+
};
603+
560604
export interface BitgoHeldBackupKeyShare {
561605
commonKeychain?: string;
562606
id: string;
@@ -714,7 +758,7 @@ export interface ITssUtils<KeyShare = EDDSA.KeyShare> {
714758
originalPasscodeEncryptionCode?: string;
715759
isThirdPartyBackup?: boolean;
716760
}): Promise<KeychainsTriplet>;
717-
signTxRequest(params: { txRequest: string | TxRequest; prv: string; reqId: IRequestTracer }): Promise<TxRequest>;
761+
signTxRequest(params: TssSignTxRequestParamsWithPrv): Promise<TxRequest>;
718762
signTxRequestForMessage(params: TSSParams): Promise<TxRequest>;
719763
signEddsaTssUsingExternalSigner(
720764
txRequest: string | TxRequest,

modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ import {
3030
TSSParamsForMessage,
3131
TSSParamsForMessageWithPrv,
3232
TSSParamsWithPrv,
33+
TssSignTxRequestParamsWithPrv,
34+
TssTxRecipientSource,
3335
TxRequest,
3436
} from '../baseTypes';
3537
import { getTxRequest } from '../../../tss';
3638
import { AShare, DShare, EncryptedNShare, SendShareType, SShare, WShare, OShare } from '../../../tss/ecdsa/types';
3739
import { createShareProof, generateGPGKeyPair, getBitgoGpgPubKey } from '../../opengpgUtils';
3840
import { BitGoBase } from '../../../bitgoBase';
41+
import { InvalidTransactionError } from '../../../errors';
3942
import { verifyWalletSignature } from '../../../tss/ecdsa/ecdsa';
4043
import { signMessageWithDerivedEcdhKey, verifyEcdhSignature } from '../../../ecdh';
4144
import { getTxRequestChallenge } from '../../../tss/common';
@@ -745,6 +748,16 @@ export class EcdsaUtils extends BaseEcdsaUtils {
745748
const unsignedTx =
746749
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
747750

751+
if (
752+
'recipientSource' in params &&
753+
params.recipientSource === TssTxRecipientSource.Explicit &&
754+
!params.txParams?.recipients?.length
755+
) {
756+
throw new InvalidTransactionError(
757+
'recipientSource "explicit" requires txParams.recipients with at least one recipient.'
758+
);
759+
}
760+
748761
// For ICP transactions, the HSM signs the serializedTxHex, while the user signs the signableHex separately.
749762
// Verification cannot be performed directly on the signableHex alone. However, we can parse the serializedTxHex
750763
// to regenerate the signableHex and compare it against the provided value for verification.
@@ -862,9 +875,11 @@ export class EcdsaUtils extends BaseEcdsaUtils {
862875
* @param {string | TxRequest} params.txRequest - transaction request object or id
863876
* @param {string} params.prv - decrypted private key
864877
* @param {string} params.reqId - request id
878+
* @param params.recipientSource - optional; use TssTxRecipientSource.Explicit with a non-empty
879+
* txParams.recipients list when you want TypeScript to enforce passing recipient details at compile time.
865880
* @returns {Promise<TxRequest>} fully signed TxRequest object
866881
*/
867-
async signTxRequest(params: TSSParamsWithPrv): Promise<TxRequest> {
882+
async signTxRequest(params: TssSignTxRequestParamsWithPrv): Promise<TxRequest> {
868883
this.bitgo.setRequestTracer(params.reqId);
869884
return this.signRequestBase(params, RequestType.tx);
870885
}

modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,14 @@ import {
4444
TSSParamsForMessage,
4545
TSSParamsForMessageWithPrv,
4646
TSSParamsWithPrv,
47+
TssSignTxRequestParamsWithPrv,
48+
TssTxRecipientSource,
4749
TxRequest,
4850
} from '../baseTypes';
4951
import { BaseEcdsaUtils } from './base';
5052
import { EcdsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './ecdsaMPCv2KeyGenSender';
5153
import { envRequiresBitgoPubGpgKeyConfig, isBitgoMpcPubKey } from '../../../tss/bitgoPubKeys';
54+
import { InvalidTransactionError } from '../../../errors';
5255

5356
export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
5457
private static readonly DKLS23_SIGNING_USER_GPG_KEY = 'DKLS23_SIGNING_USER_GPG_KEY';
@@ -697,10 +700,12 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
697700
* @param {string} params.prv - decrypted private key
698701
* @param {string} params.reqId - request id
699702
* @param {string} params.mpcv2PartyId - party id for the signer involved in this mpcv2 request (either 0 for user or 1 for backup)
703+
* @param params.recipientSource - optional; use TssTxRecipientSource.Explicit with a non-empty txParams.recipients
704+
* list when you want TypeScript to enforce passing recipient details at compile time.
700705
* @returns {Promise<TxRequest>} fully signed TxRequest object
701706
*/
702707

703-
async signTxRequest(params: TSSParamsWithPrv): Promise<TxRequest> {
708+
async signTxRequest(params: TssSignTxRequestParamsWithPrv): Promise<TxRequest> {
704709
this.bitgo.setRequestTracer(params.reqId);
705710
return this.signRequestBase(params, RequestType.tx);
706711
}
@@ -741,6 +746,16 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
741746
const unsignedTx =
742747
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
743748

749+
if (
750+
'recipientSource' in params &&
751+
params.recipientSource === TssTxRecipientSource.Explicit &&
752+
!params.txParams?.recipients?.length
753+
) {
754+
throw new InvalidTransactionError(
755+
'recipientSource "explicit" requires txParams.recipients with at least one recipient.'
756+
);
757+
}
758+
744759
// For ICP transactions, the HSM signs the serializedTxHex, while the user signs the signableHex separately.
745760
// Verification cannot be performed directly on the signableHex alone. However, we can parse the serializedTxHex
746761
// to regenerate the signableHex and compare it against the provided value for verification.

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
SignatureShareType,
3131
TSSParamsForMessageWithPrv,
3232
TSSParamsWithPrv,
33+
TssSignTxRequestParamsWithPrv,
3334
TxRequest,
3435
UnsignedTransactionTss,
3536
} from '../baseTypes';
@@ -571,7 +572,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
571572
@param params - parameters for signing the transaction request
572573
* @returns {Promise<TxRequest>} fully signed TxRequest object
573574
*/
574-
async signTxRequest(params: TSSParamsWithPrv): Promise<TxRequest> {
575+
async signTxRequest(params: TssSignTxRequestParamsWithPrv): Promise<TxRequest> {
575576
return this.signRequestBase(params, RequestType.tx);
576577
}
577578

0 commit comments

Comments
 (0)