Skip to content

Commit f116512

Browse files
feat(sdk-core): add explicit recipient mode typing for TSS signTxRequest
Introduce TssTxRecipientSource and TssSignTxRequestParams so callers can opt into compile-time enforcement of non-empty txParams.recipients via recipientSource Explicit. Default resolved behavior matches existing optional txParams. ECDSA signing validates Explicit at runtime for non-TS callers. ITssUtils.signTxRequest uses the new param type. Add MPCv2 unit test for Explicit. BREAKING CHANGE: ITssUtils.signTxRequest is now typed as TssSignTxRequestParamsWithPrv instead of a minimal inline shape. TypeScript consumers that implement or narrow this interface may need signature updates; runtime behavior for existing callers is unchanged. Refs: WAL-375 #8462 WAL-375
1 parent 27f3db2 commit f116512

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';
@@ -221,7 +221,7 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
221221
throw new Error('Method not implemented.');
222222
}
223223

224-
signTxRequest(params: TSSParamsWithPrv): Promise<TxRequest> {
224+
signTxRequest(params: TssSignTxRequestParamsWithPrv): Promise<TxRequest> {
225225
throw new Error('Method not implemented.');
226226
}
227227

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 { EncryptionVersion, IEncryptionSession, 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';
@@ -545,16 +545,6 @@ export interface EncryptedSignerShareRecord extends ShareBaseRecord {
545545
type: EncryptedSignerShareType;
546546
}
547547

548-
export type TSSParamsWithPrv = TSSParams & {
549-
prv: string;
550-
mpcv2PartyId?: 0 | 1;
551-
};
552-
553-
export type TSSParamsForMessageWithPrv = TSSParamsForMessage & {
554-
prv: string;
555-
mpcv2PartyId?: 0 | 1;
556-
};
557-
558548
export type BitgoPubKeyType = 'nitro' | 'onprem';
559549

560550
export type TSSParams = {
@@ -570,6 +560,60 @@ export type TSSParamsForMessage = TSSParams & {
570560
bufferToSign: Buffer;
571561
};
572562

563+
/** At least one recipient (when using `recipientSource: TssTxRecipientSource.Explicit`). */
564+
export type NonEmptyRecipientList = [ITransactionRecipient, ...ITransactionRecipient[]];
565+
566+
/** txParams including a non-empty recipients list for strict signing verification typing. */
567+
export type TransactionParamsWithMandatoryRecipients = TransactionParams & {
568+
recipients: NonEmptyRecipientList;
569+
};
570+
571+
export const TssTxRecipientSource = {
572+
/** Require txParams.recipients with at least one entry (enforced by TypeScript for this branch). */
573+
Explicit: 'explicit',
574+
/**
575+
* Default: txParams may be omitted or partial; verification uses coin-specific rules
576+
* (for example recipients from txRequest context).
577+
*/
578+
Resolved: 'resolved',
579+
} as const;
580+
581+
export type TssTxRecipientSource = (typeof TssTxRecipientSource)[keyof typeof TssTxRecipientSource];
582+
583+
export type TssSignTxExplicitRecipientParams = {
584+
txRequest: string | TxRequest;
585+
reqId: IRequestTracer;
586+
apiVersion?: ApiVersion;
587+
recipientSource: typeof TssTxRecipientSource.Explicit;
588+
txParams: TransactionParamsWithMandatoryRecipients;
589+
};
590+
591+
export type TssSignTxResolvedRecipientParams = {
592+
txRequest: string | TxRequest;
593+
reqId: IRequestTracer;
594+
apiVersion?: ApiVersion;
595+
recipientSource?: typeof TssTxRecipientSource.Resolved;
596+
txParams?: TransactionParams;
597+
};
598+
599+
/**
600+
* Parameters for TSS transaction signing ({@link ITssUtils.signTxRequest}).
601+
* Set {@link TssTxRecipientSource.Explicit} to require a non-empty txParams.recipients array at compile time.
602+
*/
603+
export type TssSignTxRequestParams = TssSignTxExplicitRecipientParams | TssSignTxResolvedRecipientParams;
604+
605+
export type TssSignTxRequestParamsWithPrv = TssSignTxRequestParams & {
606+
prv: string;
607+
mpcv2PartyId?: 0 | 1;
608+
};
609+
610+
export type TSSParamsWithPrv = TssSignTxRequestParamsWithPrv;
611+
612+
export type TSSParamsForMessageWithPrv = TSSParamsForMessage & {
613+
prv: string;
614+
mpcv2PartyId?: 0 | 1;
615+
};
616+
573617
export interface BitgoHeldBackupKeyShare {
574618
commonKeychain?: string;
575619
id: string;
@@ -728,7 +772,7 @@ export interface ITssUtils<KeyShare = EDDSA.KeyShare> {
728772
isThirdPartyBackup?: boolean;
729773
encryptionVersion?: EncryptionVersion;
730774
}): Promise<KeychainsTriplet>;
731-
signTxRequest(params: { txRequest: string | TxRequest; prv: string; reqId: IRequestTracer }): Promise<TxRequest>;
775+
signTxRequest(params: TssSignTxRequestParamsWithPrv): Promise<TxRequest>;
732776
signTxRequestForMessage(params: TSSParams): Promise<TxRequest>;
733777
signEddsaTssUsingExternalSigner(
734778
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
@@ -45,12 +45,15 @@ import {
4545
TSSParamsForMessage,
4646
TSSParamsForMessageWithPrv,
4747
TSSParamsWithPrv,
48+
TssSignTxRequestParamsWithPrv,
49+
TssTxRecipientSource,
4850
TxRequest,
4951
isV2Envelope,
5052
} from '../baseTypes';
5153
import { BaseEcdsaUtils } from './base';
5254
import { EcdsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './ecdsaMPCv2KeyGenSender';
5355
import { envRequiresBitgoPubGpgKeyConfig, isBitgoMpcPubKey } from '../../../tss/bitgoPubKeys';
56+
import { InvalidTransactionError } from '../../../errors';
5457

5558
export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
5659
private static readonly DKLS23_SIGNING_USER_GPG_KEY = 'DKLS23_SIGNING_USER_GPG_KEY';
@@ -732,10 +735,12 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
732735
* @param {string} params.prv - decrypted private key
733736
* @param {string} params.reqId - request id
734737
* @param {string} params.mpcv2PartyId - party id for the signer involved in this mpcv2 request (either 0 for user or 1 for backup)
738+
* @param params.recipientSource - optional; use TssTxRecipientSource.Explicit with a non-empty txParams.recipients
739+
* list when you want TypeScript to enforce passing recipient details at compile time.
735740
* @returns {Promise<TxRequest>} fully signed TxRequest object
736741
*/
737742

738-
async signTxRequest(params: TSSParamsWithPrv): Promise<TxRequest> {
743+
async signTxRequest(params: TssSignTxRequestParamsWithPrv): Promise<TxRequest> {
739744
this.bitgo.setRequestTracer(params.reqId);
740745
return this.signRequestBase(params, RequestType.tx);
741746
}
@@ -776,6 +781,16 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
776781
const unsignedTx =
777782
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
778783

784+
if (
785+
'recipientSource' in params &&
786+
params.recipientSource === TssTxRecipientSource.Explicit &&
787+
!params.txParams?.recipients?.length
788+
) {
789+
throw new InvalidTransactionError(
790+
'recipientSource "explicit" requires txParams.recipients with at least one recipient.'
791+
);
792+
}
793+
779794
// For ICP transactions, the HSM signs the serializedTxHex, while the user signs the signableHex separately.
780795
// Verification cannot be performed directly on the signableHex alone. However, we can parse the serializedTxHex
781796
// 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
isV2Envelope,
@@ -610,7 +611,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
610611
@param params - parameters for signing the transaction request
611612
* @returns {Promise<TxRequest>} fully signed TxRequest object
612613
*/
613-
async signTxRequest(params: TSSParamsWithPrv): Promise<TxRequest> {
614+
async signTxRequest(params: TssSignTxRequestParamsWithPrv): Promise<TxRequest> {
614615
return this.signRequestBase(params, RequestType.tx);
615616
}
616617

0 commit comments

Comments
 (0)