diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts index 43272d7f36e..59d55cb7e5c 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts @@ -21,7 +21,6 @@ import { Eddsa, EncryptedSignerShareType, ExchangeCommitmentResponse, - InvalidTransactionError, Keychain, KeyShare, RequestTracer, @@ -128,14 +127,9 @@ describe('TSS Utils:', async function () { }, }; - // Sol TSS unsigned tx bytes — same fixtures as eddsaMPCv2/signTxRequest.ts - const solTssSignableHex = - '02010206c2d5b5f4fb9a9bcd8a2f303e4d06f78d8ded300713f456da2abff0b3ea0185aa051a34bc8acd438763976f96876115050f73828553566d111d7ac8bffebf587c4f5f5987bfe26aa66013efd96d36360f2b4336c91f993259fb56051305614d42f2ea13f8ff9d7958dbf269c6e36bfdf5cb5c43de4b4e1d3efb7dab3d5d028604000000000000000000000000000000000000000000000000000000000000000006a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea94000003a621f6d1cc4b8fb2a739aa08e4034da0fc588ece3bd857630de30f7edde45dd0204030205010404000000040200030c02000000f0a29a3b00000000'; - const solTssSerializedTxHex = `02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003bc9df0b397bec2ed3b6444a8c33f38267cc08b5fb2a7d37e26b6c487e26d15b7c07830eb78e26a88db5de4aa6986a327f09aed8c01533e5b972748ddf60b80f${solTssSignableHex}`; - const txRequest = { txRequestId: 'randomId', - unsignedTxs: [{ signableHex: solTssSignableHex, serializedTxHex: solTssSerializedTxHex }], + unsignedTxs: [{ signableHex: 'MPC on a Friday night', serializedTxHex: 'MPC on a Friday night' }], signatureShares: [ { from: 'bitgo', @@ -662,21 +656,13 @@ describe('TSS Utils:', async function () { txRequestId: 'v2-signing-test', unsignedTxs: [ { - serializedTxHex: solTssSerializedTxHex, - signableHex: solTssSignableHex, + serializedTxHex: 'test-payload', + signableHex: 'deadbeef', derivationPath: 'm/0', }, ], date: new Date().toISOString(), - intent: { - intentType: 'payment', - recipients: [ - { - address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' }, - amount: { value: '999990000', symbol: 'tsol' }, - }, - ], - }, + intent: { intentType: 'payment' }, latest: true, state: 'pendingUserSignature', walletType: 'hot', @@ -686,10 +672,6 @@ describe('TSS Utils:', async function () { userId: 'userId', }; - beforeEach(function () { - sandbox.stub(baseCoin, 'verifyTransaction').resolves(true); - }); - it('v2 R-share round-trip: encrypt via commitment, verify envelope, decrypt via createRShare', async function () { const passphrase = 'test-passphrase'; const prv = JSON.stringify(validUserSigningMaterial); @@ -734,20 +716,14 @@ describe('TSS Utils:', async function () { transactions: [], unsignedTxs: [ { - serializedTxHex: solTssSerializedTxHex, - signableHex: solTssSignableHex, + serializedTxHex: 'MPC on a Friday night', + signableHex: 'MPC on a Friday night', derivationPath: 'm/0', }, ], date: new Date().toISOString(), intent: { intentType: 'payment', - recipients: [ - { - address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' }, - amount: { value: '999990000', symbol: 'tsol' }, - }, - ], }, latest: true, state: 'pendingUserSignature', @@ -759,8 +735,6 @@ describe('TSS Utils:', async function () { }; beforeEach(async function () { - sandbox.stub(baseCoin, 'verifyTransaction').resolves(true); - const userSignShare = validUserSignShare; const rShare = userSignShare.rShares[3]; const signatureShare: SignatureShareRecord = { @@ -831,20 +805,14 @@ describe('TSS Utils:', async function () { transactions: [], unsignedTxs: [ { - serializedTxHex: solTssSerializedTxHex, - signableHex: solTssSignableHex, + serializedTxHex: 'MPC on a Friday night', + signableHex: 'MPC on a Friday night', derivationPath: 'm/0', }, ], date: new Date().toISOString(), intent: { intentType: 'payment', - recipients: [ - { - address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' }, - amount: { value: '999990000', symbol: 'tsol' }, - }, - ], }, latest: true, state: 'pendingUserSignature', @@ -856,8 +824,6 @@ describe('TSS Utils:', async function () { }; beforeEach(async function () { - sandbox.stub(baseCoin, 'verifyTransaction').resolves(true); - const userSignShare = validUserSignShare; const rShare = userSignShare.rShares[3]; const signatureShare: SignatureShareRecord = { @@ -920,328 +886,6 @@ describe('TSS Utils:', async function () { }); }); - describe('signTxRequest resolveEffectiveTxParams guard:', function () { - const txRequestId = 'randomid-guard'; - const baseTxRequest: TxRequest = { - txRequestId, - transactions: [], - unsignedTxs: [ - { - serializedTxHex: solTssSerializedTxHex, - signableHex: solTssSignableHex, - derivationPath: 'm/0', - }, - ], - date: new Date().toISOString(), - intent: { - intentType: 'payment', - recipients: [ - { - address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' }, - amount: { value: '999990000', symbol: 'tsol' }, - }, - ], - }, - latest: true, - state: 'pendingUserSignature', - walletType: 'hot', - walletId: 'walletId', - policiesChecked: true, - version: 1, - userId: 'userId', - }; - - it('throws InvalidTransactionError when txParams is absent and intent has no recipients', async function () { - const maliciousTxRequest: TxRequest = { - ...baseTxRequest, - intent: { intentType: 'stakingAuthorize' }, - }; - await tssUtils - .signTxRequest({ - txRequest: maliciousTxRequest, - prv: JSON.stringify(validUserSigningMaterial), - reqId, - }) - .should.be.rejectedWith(InvalidTransactionError); - }); - - it('uses intent recipients when txParams is absent', async function () { - const verifyStub = sandbox.stub(baseCoin, 'verifyTransaction').resolves(true); - - const userSignShare = validUserSignShare; - const rShare = userSignShare.rShares[3]; - const signatureShare: SignatureShareRecord = { - from: SignatureShareType.USER, - to: SignatureShareType.BITGO, - share: rShare.r + rShare.R, - }; - await nockSendSignatureShare({ - walletId: wallet.id(), - txRequestId: baseTxRequest.txRequestId, - signatureShare, - }); - const signatureShare2: SignatureShareRecord = { - from: SignatureShareType.BITGO, - to: SignatureShareType.USER, - share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, - }; - const response = { txRequests: [{ ...baseTxRequest, signatureShares: [signatureShare2] }] }; - await nockGetTxRequest({ walletId: wallet.id(), txRequestId: baseTxRequest.txRequestId, response }); - const bitgoToUserCommitmentShare: CommitmentShareRecord = { - from: SignatureShareType.BITGO, - to: SignatureShareType.USER, - type: CommitmentType.COMMITMENT, - share: validBitgoToUserSignShare.rShares[1].commitment, - }; - await nockExchangeCommitments({ - walletId: wallet.id(), - txRequestId: baseTxRequest.txRequestId, - response: { commitmentShare: bitgoToUserCommitmentShare }, - }); - - await tssUtils.signTxRequest({ - txRequest: baseTxRequest, - prv: JSON.stringify(validUserSigningMaterial), - reqId, - }); - - verifyStub.calledOnce.should.be.true(); - const verifyArgs = verifyStub.firstCall.args[0]; - verifyArgs.txParams.recipients?.[0].address.should.equal('HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs'); - }); - - it('does not throw for allowlisted no-recipient intentType (deactivate)', async function () { - const verifyStub = sandbox.stub(baseCoin, 'verifyTransaction').resolves(true); - - const deactivateTxRequest: TxRequest = { - ...baseTxRequest, - intent: { intentType: 'deactivate' }, - }; - - const userSignShare = validUserSignShare; - const rShare = userSignShare.rShares[3]; - const signatureShare: SignatureShareRecord = { - from: SignatureShareType.USER, - to: SignatureShareType.BITGO, - share: rShare.r + rShare.R, - }; - await nockSendSignatureShare({ - walletId: wallet.id(), - txRequestId: deactivateTxRequest.txRequestId, - signatureShare, - }); - const signatureShare2: SignatureShareRecord = { - from: SignatureShareType.BITGO, - to: SignatureShareType.USER, - share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, - }; - const response = { txRequests: [{ ...deactivateTxRequest, signatureShares: [signatureShare2] }] }; - await nockGetTxRequest({ - walletId: wallet.id(), - txRequestId: deactivateTxRequest.txRequestId, - response, - }); - const bitgoToUserCommitmentShare: CommitmentShareRecord = { - from: SignatureShareType.BITGO, - to: SignatureShareType.USER, - type: CommitmentType.COMMITMENT, - share: validBitgoToUserSignShare.rShares[1].commitment, - }; - await nockExchangeCommitments({ - walletId: wallet.id(), - txRequestId: deactivateTxRequest.txRequestId, - response: { commitmentShare: bitgoToUserCommitmentShare }, - }); - - await tssUtils.signTxRequest({ - txRequest: deactivateTxRequest, - prv: JSON.stringify(validUserSigningMaterial), - reqId, - }); - - verifyStub.calledOnce.should.be.true(); - }); - - describe('consolidate intent', function () { - // Intent recipient amount is a build-time snapshot that drifts from the swept balance in the tx. - const consolidateTxRequest: TxRequest = { - ...baseTxRequest, - intent: { - intentType: 'consolidate', - consolidateId: '68a7d5d0c66e74e216b97173bd558c6d', - recipients: [ - { - address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' }, - amount: { value: '999985000', symbol: 'tsol' }, - }, - ], - }, - }; - - it('verifies sweep-to-root instead of snapshot intent recipients', async function () { - sandbox - .stub(wallet, 'coinSpecific') - .returns({ rootAddress: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs', customChangeWalletId: '' }); - const verifySpy = sandbox.spy(baseCoin, 'verifyTransaction'); - - const rShare = validUserSignShare.rShares[3]; - await nockSendSignatureShare({ - walletId: wallet.id(), - txRequestId: consolidateTxRequest.txRequestId, - signatureShare: { from: SignatureShareType.USER, to: SignatureShareType.BITGO, share: rShare.r + rShare.R }, - }); - const signatureShare2: SignatureShareRecord = { - from: SignatureShareType.BITGO, - to: SignatureShareType.USER, - share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, - }; - await nockGetTxRequest({ - walletId: wallet.id(), - txRequestId: consolidateTxRequest.txRequestId, - response: { txRequests: [{ ...consolidateTxRequest, signatureShares: [signatureShare2] }] }, - }); - await nockExchangeCommitments({ - walletId: wallet.id(), - txRequestId: consolidateTxRequest.txRequestId, - response: { - commitmentShare: { - from: SignatureShareType.BITGO, - to: SignatureShareType.USER, - type: CommitmentType.COMMITMENT, - share: validBitgoToUserSignShare.rShares[1].commitment, - }, - }, - }); - - await tssUtils.signTxRequest({ - txRequest: consolidateTxRequest, - prv: JSON.stringify(validUserSigningMaterial), - reqId, - }); - - verifySpy.calledOnce.should.be.true(); - const verifyArgs = verifySpy.firstCall.args[0]; - should.not.exist(verifyArgs.txParams.recipients); - should(verifyArgs.txPrebuild.consolidateId).equal('68a7d5d0c66e74e216b97173bd558c6d'); - should(verifyArgs.verification).deepEqual({ consolidationToBaseAddress: true }); - }); - - it('rejects a consolidation that does not sweep to the wallet root address', async function () { - sandbox - .stub(wallet, 'coinSpecific') - .returns({ rootAddress: '5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe', customChangeWalletId: '' }); - - await tssUtils - .signTxRequest({ - txRequest: consolidateTxRequest, - prv: JSON.stringify(validUserSigningMaterial), - reqId, - }) - .should.be.rejectedWith('tx outputs does not match with expected address'); - }); - - describe('token consolidation', function () { - // SPL token sweep: the output is the root's associated token account, not the root itself. - const tokenConsolidationTxHex = - '02b7c2c7829eded4e8f947c90ed3b9afce71f616eb47dfcfbf4b765778149060013acb33b9f67fdd9c2512f48fac4e4c049eab93829b69404f3bd166fe3242c90700000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020104096da690bd558fd8634ac14f1645d8095afd0caea5953578c596e3c0dea38305ee7298bfac55101f177735659d42ed6be890ef3a1d204d9e33f32e24c5635327ca66d1fe00826e5a4f759f87b279c1aee19cce5301af4ed66ae17db48b201ed6c2a0e8a28bf565627f1ab8a34b1a95ee1b0a2a39084f1f0e2acb1c394b20185d8e0b22657c8d9c4ce5f6495efb6410c199011530f90e3ab9d8d1e4206f9ae0ffeb0000000000000000000000000000000000000000000000000000000000000000c5f9fb32f49111ab20c33f2598fc836c113e291881ac21ee29169394011244e406a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea940000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9de13c74d2b4d948e1608ea6eebdafe75bc2f995aad11b21d1e2c94f2a2d12f6802050303070004040000000804020604010a0ce0076bb20400000006'; - const tokenConsolidateTxRequest: TxRequest = { - ...consolidateTxRequest, - unsignedTxs: [ - { - serializedTxHex: tokenConsolidationTxHex, - signableHex: tokenConsolidationTxHex.slice(2 + 2 * 128), - derivationPath: 'm/0', - }, - ], - }; - - it('verifies the output is the wallet root token account', async function () { - sandbox - .stub(wallet, 'coinSpecific') - .returns({ rootAddress: 'HBxZShcE86UMmF93KUM8eWJKqeEXi5cqWCLYLMMhqMYm', customChangeWalletId: '' }); - const verifySpy = sandbox.spy(baseCoin, 'verifyTransaction'); - // Stop after verification; signing itself is covered above. - sandbox.stub(tssUtils, 'pickBitgoPubGpgKeyForSigning').rejects(new Error('verified')); - - await tssUtils - .signTxRequest({ - txRequest: tokenConsolidateTxRequest, - prv: JSON.stringify(validUserSigningMaterial), - reqId, - }) - .should.be.rejectedWith('verified'); - (await verifySpy.firstCall.returnValue).should.be.true(); - }); - - it('rejects a token output that is not the wallet root token account', async function () { - sandbox - .stub(wallet, 'coinSpecific') - .returns({ rootAddress: '5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe', customChangeWalletId: '' }); - - await tssUtils - .signTxRequest({ - txRequest: tokenConsolidateTxRequest, - prv: JSON.stringify(validUserSigningMaterial), - reqId, - }) - .should.be.rejectedWith('tx outputs does not match with expected address'); - }); - }); - }); - }); - - describe('signEddsaTssUsingExternalSigner resolveEffectiveTxParams guard:', function () { - const externalGuardTxRequest: TxRequest = { - txRequestId: 'randomid-external-guard', - transactions: [], - unsignedTxs: [ - { - serializedTxHex: solTssSerializedTxHex, - signableHex: solTssSignableHex, - derivationPath: 'm/0', - }, - ], - date: new Date().toISOString(), - intent: { - intentType: 'payment', - recipients: [ - { - address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' }, - amount: { value: '999990000', symbol: 'tsol' }, - }, - ], - }, - latest: true, - state: 'pendingUserSignature', - walletType: 'hot', - walletId: 'walletId', - policiesChecked: true, - version: 1, - userId: 'userId', - }; - - it('throws InvalidTransactionError before external signer callbacks when intent has no recipients', async function () { - const commitmentGen = sandbox.stub().rejects(new Error('should not run')); - const maliciousTxRequest: TxRequest = { - ...externalGuardTxRequest, - intent: { intentType: 'stakingAuthorize' }, - }; - await tssUtils - .signEddsaTssUsingExternalSigner( - maliciousTxRequest, - commitmentGen, - async function () { - throw new Error('should not run'); - }, - async function () { - throw new Error('should not run'); - } - ) - .should.be.rejectedWith(InvalidTransactionError); - commitmentGen.notCalled.should.be.true(); - }); - }); - describe('signTxRequestForMessage:', function () { const txRequestId = 'randomid-abc'; const messageRaw = 'hello world'; diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts index acfe34988bd..1e6d03300c4 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts @@ -484,53 +484,20 @@ describe('signTxRequest:', function () { }); }); - describe('consolidate intent', function () { - // Intent recipient amount is a build-time snapshot that drifts from the swept balance in the tx. + it('does not throw for allowlisted no-recipient intentType (consolidate)', async function () { + sandbox.stub(baseCoin, 'verifyTransaction').resolves(true); + const nockPromises = await getNockPromisesForEddsaSigning(txRequest); + await Promise.all(nockPromises); + const consolidateTxRequest: TxRequest = { ...txRequest, - intent: { - intentType: 'consolidate', - consolidateId: '68a7d5d0c66e74e216b97173bd558c6d', - recipients: [ - { - address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' }, - amount: { value: '999985000', symbol: 'sol' }, - }, - ], - }, + intent: { intentType: 'consolidate' } as any, }; - - it('verifies sweep-to-root instead of snapshot intent recipients', async function () { - // Fee payer stays the original root, so this also covers the consolidation fee payer exemption. - sandbox - .stub(wallet, 'coinSpecific') - .returns({ rootAddress: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs', customChangeWalletId: '' }); - const verifySpy = sandbox.spy(baseCoin, 'verifyTransaction'); - const nockPromises = await getNockPromisesForEddsaSigning(consolidateTxRequest); - await Promise.all(nockPromises); - - await tssUtils.signTxRequest({ - txRequest: consolidateTxRequest, - prv: Buffer.from(userKeyShare).toString('base64'), - reqId, - }); - - verifySpy.calledOnce.should.be.true(); - verifySpy.firstCall.args[0].should.containDeep({ - txPrebuild: { consolidateId: '68a7d5d0c66e74e216b97173bd558c6d' }, - verification: { consolidationToBaseAddress: true }, - }); - verifySpy.firstCall.args[0].txParams.should.not.have.property('recipients'); - }); - - it('rejects a consolidation that does not sweep to the wallet root address', async function () { - await tssUtils - .signTxRequest({ - txRequest: consolidateTxRequest, - prv: Buffer.from(userKeyShare).toString('base64'), - reqId, - }) - .should.be.rejectedWith('tx outputs does not match with expected address'); + const userPrvBase64 = Buffer.from(userKeyShare).toString('base64'); + await tssUtils.signTxRequest({ + txRequest: consolidateTxRequest, + prv: userPrvBase64, + reqId, }); }); diff --git a/modules/bitgo/test/v2/unit/signTransactionVerification.ts b/modules/bitgo/test/v2/unit/signTransactionVerification.ts index c6943d98a10..b7a8945a3bc 100644 --- a/modules/bitgo/test/v2/unit/signTransactionVerification.ts +++ b/modules/bitgo/test/v2/unit/signTransactionVerification.ts @@ -6,14 +6,7 @@ import 'should'; import { BitGoAPI } from '@bitgo/sdk-api'; import { TestBitGo } from '@bitgo/sdk-test'; import { Tbtc } from '@bitgo/sdk-coin-btc'; -import { - common, - BaseCoin, - BitGoBase, - InvalidTransactionError, - Wallet, - WalletSignTransactionOptions, -} from '@bitgo/sdk-core'; +import { common, BaseCoin, BitGoBase, Wallet, WalletSignTransactionOptions } from '@bitgo/sdk-core'; describe('Wallet signTransaction with verifyTxParams', function () { let wallet: Wallet; @@ -158,18 +151,4 @@ describe('Wallet signTransaction with verifyTxParams', function () { assert.strictEqual(verifyParams.txPrebuild.txHex, 'mock-tx-hex'); assert.deepStrictEqual(verifyParams.txParams, verifyTxParams.txParams); }); - - it('should throw when verifyTxParams is provided without txHex or TSS txRequestId', async function () { - const signParams: WalletSignTransactionOptions = { - txPrebuild: {}, - verifyTxParams: { - txParams: { - recipients: [{ address: 'test-address', amount: '1000' }], - }, - }, - }; - - await wallet.signTransaction(signParams).should.be.rejectedWith(InvalidTransactionError); - sinon.assert.notCalled(verifyTransactionStub); - }); }); diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts index ba91ab4e0b5..906ae8a717f 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts @@ -1,7 +1,7 @@ import { EncryptionVersion, IRequestTracer } from '../../../api'; import * as openpgp from 'openpgp'; import { Key, readKey, SerializedKeyPair } from 'openpgp'; -import { IBaseCoin, KeychainsTriplet, TransactionParams } from '../../baseCoin'; +import { IBaseCoin, KeychainsTriplet } from '../../baseCoin'; import { BitGoBase } from '../../bitgoBase'; import { Keychain, KeyIndices, WebauthnKeyEncryptionInfo } from '../../keychain'; import { getTxRequest } from '../../tss'; @@ -269,9 +269,7 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil txRequest: string | TxRequest, externalSignerCommitmentGenerator: CustomCommitmentGeneratingFunction, externalSignerRShareGenerator: CustomRShareGeneratingFunction, - externalSignerGShareGenerator: CustomGShareGeneratingFunction, - _reqId?: IRequestTracer, - _txParams?: TransactionParams + externalSignerGShareGenerator: CustomGShareGeneratingFunction ): Promise { throw new Error('Method not implemented.'); } diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts index df64526fa4c..7272a6dec2d 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts @@ -934,9 +934,7 @@ export interface ITssUtils { txRequest: string | TxRequest, externalSignerCommitmentGenerator: CustomCommitmentGeneratingFunction, externalSignerRShareGenerator: CustomRShareGeneratingFunction, - externalSignerGShareGenerator: CustomGShareGeneratingFunction, - reqId?: IRequestTracer, - txParams?: TransactionParams + externalSignerGShareGenerator: CustomGShareGeneratingFunction ): Promise; signEcdsaTssUsingExternalSigner( params: TSSParams | TSSParamsForMessage, diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts index 5a49de68754..400d67402c8 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts @@ -40,7 +40,7 @@ import { InvalidTransactionError } from '../../../errors'; import { CreateEddsaBitGoKeychainParams, CreateEddsaKeychainParams, KeyShare, YShare } from './types'; import baseTSSUtils from '../baseTSSUtils'; import { BaseEddsaUtils } from './base'; -import { KeychainsTriplet, TransactionParams } from '../../../baseCoin'; +import { KeychainsTriplet } from '../../../baseCoin'; import { exchangeEddsaCommitments } from '../../../tss/common'; import { Ed25519Bip32HdTree } from '@bitgo/sdk-lib-mpc'; import { EncryptionVersion, IRequestTracer } from '../../../../api'; @@ -48,7 +48,6 @@ import { envRequiresBitgoPubGpgKeyConfig, getBitgoMpcGpgPubKey, isBitgoMpcPubKey import { EnvironmentName } from '../../../environments'; import { readKey } from 'openpgp'; import type { EddsaKeyGenCallbacks } from '../../../wallet/iWallets'; -import { resolveTssVerifyTransactionOptions } from '../recipientUtils'; /** * Utility functions for TSS work flows. @@ -658,8 +657,7 @@ export class EddsaUtils extends baseTSSUtils { externalSignerCommitmentGenerator: CustomCommitmentGeneratingFunction, externalSignerRShareGenerator: CustomRShareGeneratingFunction, externalSignerGShareGenerator: CustomGShareGeneratingFunction, - reqId?: IRequestTracer, - txParams?: TransactionParams + reqId?: IRequestTracer ): Promise { let txRequestResolved: TxRequest; let txRequestId: string; @@ -671,8 +669,6 @@ export class EddsaUtils extends baseTSSUtils { txRequestId = txRequest.txRequestId; } - await this.verifyEdDsaTxRequestBeforeSigning(txRequestResolved, txParams); - const { apiVersion } = txRequestResolved; const bitgoGpgKey = await this.pickBitgoPubGpgKeyForSigning(false, reqId, txRequestResolved.enterpriseId); @@ -770,8 +766,6 @@ export class EddsaUtils extends baseTSSUtils { ); unsignedTx = apiVersion === 'full' ? txRequestResolved.transactions![0].unsignedTx : txRequestResolved.unsignedTxs[0]; - const txParams = 'txParams' in params ? params.txParams : undefined; - await this.verifyEdDsaTxRequestBeforeSigning(txRequestResolved, txParams); } else if (requestType === RequestType.message) { assert(txRequestResolved.messages?.length, 'Unable to find messages in txRequest for message signing'); const message = txRequestResolved.messages[0]; @@ -878,28 +872,6 @@ export class EddsaUtils extends baseTSSUtils { return BaseEddsaUtils.getPublicKeyFromCommonKeychain(commonKeychain); } - private async verifyEdDsaTxRequestBeforeSigning( - txRequestResolved: TxRequest, - txParams?: TransactionParams - ): Promise { - assert(txRequestResolved.transactions || txRequestResolved.unsignedTxs, 'Unable to find transactions in txRequest'); - const unsignedTx = - txRequestResolved.apiVersion === 'full' - ? txRequestResolved.transactions![0].unsignedTx - : txRequestResolved.unsignedTxs[0]; - assert(unsignedTx.signableHex, 'Missing signableHex in unsignedTx'); - await this.baseCoin.verifyTransaction({ - ...resolveTssVerifyTransactionOptions( - txRequestResolved, - unsignedTx.serializedTxHex ?? unsignedTx.signableHex, - txParams, - this.baseCoin.getChain() - ), - wallet: this.wallet, - walletType: this.wallet.multisigType(), - }); - } - createUserToBitgoCommitmentShare(commitment: string): CommitmentShareRecord { return { from: SignatureShareType.USER, diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index 7011dfcef26..d922f44553d 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -54,7 +54,7 @@ import { import { EncryptionVersion } from '../../../../api'; import { BitGoBase } from '../../../bitgoBase'; import { BaseEddsaUtils } from './base'; -import { resolveTssVerifyTransactionOptions } from '../recipientUtils'; +import { resolveEffectiveTxParams } from '../recipientUtils'; import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './eddsaMPCv2KeyGenSender'; import { EddsaMPCv2RecoveryKeyShares } from './types'; import { parseMpcV2KeyShareEnvelope } from '../keyShareEnvelope'; @@ -598,12 +598,8 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { derivationPath = unsignedTx.derivationPath; bufferContent = Buffer.from(txOrMessageToSign, 'hex'); await this.baseCoin.verifyTransaction({ - ...resolveTssVerifyTransactionOptions( - txRequest, - unsignedTx.serializedTxHex ?? txOrMessageToSign, - params.txParams, - this.baseCoin.getChain() - ), + txPrebuild: { txHex: unsignedTx.serializedTxHex ?? txOrMessageToSign }, + txParams: resolveEffectiveTxParams(txRequest, params.txParams, this.baseCoin.getChain()), wallet: this.wallet, walletType: this.wallet.multisigType(), }); diff --git a/modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts index c167386b08a..460347fb291 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts @@ -1,5 +1,4 @@ -import * as t from 'io-ts'; -import { TransactionParams, VerifyTransactionOptions } from '../../baseCoin'; +import { TransactionParams } from '../../baseCoin'; import { InvalidTransactionError } from '../../errors'; import { PopulatedIntent, TxRequest } from './baseTypes'; @@ -191,36 +190,3 @@ export function resolveEffectiveTxParams( return effectiveTxParams; } - -const ConsolidateIntent = t.intersection([ - t.type({ intentType: t.literal('consolidate') }), - t.partial({ consolidateId: t.string }), -]); - -/** - * Resolves the verifyTransaction options for signing-time verification of a TSS txRequest. - * - * Consolidation intent recipients are a server-generated, build-time balance snapshot that drifts - * from the swept amount, so they are not backfilled; sweep-to-base-address is verified instead, - * mirroring wallet.sendAccountConsolidations. - * - * @param txRequest - the transaction request containing the persisted intent - * @param txHex - the unsigned transaction to verify - * @param txParams - the caller-supplied transaction parameters (may be undefined) - * @param chainName - the base chain name; pass baseCoin.getChain() - */ -export function resolveTssVerifyTransactionOptions( - txRequest: TxRequest, - txHex: string, - txParams: TransactionParams | undefined, - chainName: string -): Pick { - if (ConsolidateIntent.is(txRequest.intent)) { - return { - txPrebuild: { txHex, consolidateId: txRequest.intent.consolidateId }, - txParams: { ...txParams }, - verification: { consolidationToBaseAddress: true }, - }; - } - return { txPrebuild: { txHex }, txParams: resolveEffectiveTxParams(txRequest, txParams, chainName) }; -} diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index e0130c883b3..8f1f1916b22 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -423,8 +423,6 @@ export interface WalletSignTransactionOptions extends WalletSignBaseOptions { txParams: TransactionParams; verification?: VerificationOptions; }; - /** Populated by wallet.verifyTxParams TSS path so signing uses the same txRequest that was verified. */ - resolvedTxRequestForSigning?: TxRequest; [index: string]: unknown; } diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 68b7d8e0daa..9afc8d033d8 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -27,7 +27,6 @@ import { getSharedSecret } from '../ecdh'; import { AddressGenerationError, IncorrectPasswordError, - InvalidTransactionError, MethodNotImplementedError, MissingEncryptedKeychainError, NeedUserSignupError, @@ -57,7 +56,6 @@ import { decodeWithCodec } from '../utils/codecs'; import { postWithCodec } from '../utils/postWithCodec'; import { EcdsaMPCv2Utils, EcdsaUtils } from '../utils/tss/ecdsa'; import EddsaUtils, { EddsaMPCv2Utils } from '../utils/tss/eddsa'; -import { resolveEffectiveTxParams } from '../utils/tss/recipientUtils'; import { RedpallasMPCv2Utils } from '../utils/tss/redpallas'; import { getTxRequestApiVersion, validateTxRequestApiVersion } from '../utils/txRequest'; import { buildParamKeys, BuildParams } from './BuildParams'; @@ -2388,41 +2386,18 @@ export class Wallet implements IWallet { params.txPrebuild = { txRequestId }; } - // Verify transaction if verifyTxParams is provided (fail closed — never skip silently). - if (params.verifyTxParams) { - const prebuild = params.txPrebuild; - if (prebuild?.txHex) { - const verifyParams = { - txPrebuild: { ...prebuild }, - txParams: params.verifyTxParams.txParams, - wallet: this, - verification: params.verifyTxParams.verification, - reqId: params.reqId, - walletType: this.multisigType(), - }; + // Verify transaction if verifyTxParams is provided + if (params.verifyTxParams && txPrebuild?.txHex) { + const verifyParams = { + txPrebuild: { ...txPrebuild }, + txParams: params.verifyTxParams.txParams, + wallet: this as IWallet, + verification: params.verifyTxParams.verification, + reqId: params.reqId, + walletType: this.multisigType() as 'onchain' | 'tss', + }; - await this.baseCoin.verifyTransaction(verifyParams); - } else if (this.multisigType() === 'tss' && prebuild?.txRequestId && typeof prebuild.txRequestId === 'string') { - const txRequest = await getTxRequest(this.bitgo, this.id(), prebuild.txRequestId, params.reqId); - assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest'); - const unsignedTx = - txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs![0]; - assert(unsignedTx.signableHex, 'Missing signableHex in unsignedTx'); - await this.baseCoin.verifyTransaction({ - txPrebuild: { txHex: unsignedTx.serializedTxHex ?? unsignedTx.signableHex }, - txParams: resolveEffectiveTxParams(txRequest, params.verifyTxParams.txParams, this.baseCoin.getChain()), - wallet: this, - verification: params.verifyTxParams.verification, - reqId: params.reqId, - walletType: this.multisigType(), - }); - // Sign the same resolved txRequest (avoid TOCTOU re-fetch before signing). - params.resolvedTxRequestForSigning = txRequest; - } else { - throw new InvalidTransactionError( - 'verifyTxParams was provided but txPrebuild does not include txHex or a TSS txRequestId.' - ); - } + await this.baseCoin.verifyTransaction(verifyParams); } if ( @@ -5039,16 +5014,13 @@ export class Wallet implements IWallet { const reqId = params.reqId || undefined; await this.tssUtils.deleteSignatureShares(txRequestId, reqId); - const txParams = params.verifyTxParams?.txParams ?? params.txPrebuild?.buildParams; - try { return await this.tssUtils.signEddsaTssUsingExternalSigner( txRequestId, params.customCommitmentGeneratingFunction, params.customRShareGeneratingFunction, params.customGShareGeneratingFunction, - reqId, - txParams + reqId ); } catch (e) { debug('failed to sign transaction %O', e); @@ -5288,7 +5260,7 @@ export class Wallet implements IWallet { throw new Error('prv required to sign transactions with TSS'); } - const txRequest: string | TxRequest = params.resolvedTxRequestForSigning ?? params.txPrebuild.txRequestId; + const txRequest: string | TxRequest = params.txPrebuild.txRequestId; const txParams: TransactionParams | undefined = params.txPrebuild.buildParams; try { diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts index 1c93d7ca997..fd187f56d4f 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts @@ -1,9 +1,5 @@ import assert from 'assert'; -import { - NO_RECIPIENT_TX_TYPES, - resolveEffectiveTxParams, - resolveTssVerifyTransactionOptions, -} from '../../../../../src/bitgo/utils/tss/recipientUtils'; +import { NO_RECIPIENT_TX_TYPES, resolveEffectiveTxParams } from '../../../../../src/bitgo/utils/tss/recipientUtils'; import { InvalidTransactionError } from '../../../../../src/bitgo/errors'; import { PopulatedIntent, TxRequest } from '../../../../../src/bitgo/utils/tss/baseTypes'; @@ -509,27 +505,4 @@ describe('recipientUtils', function () { }); }); }); - - describe('resolveTssVerifyTransactionOptions', function () { - const intentRecipients = [{ address: { address: 'addr1' }, amount: { value: '100', symbol: 'tsol' } }]; - - it('verifies consolidations as sweep-to-base-address without backfilling intent recipients', function () { - const txRequest = makeTxRequest({ - intent: { intentType: 'consolidate', consolidateId: 'consolidate-id', recipients: intentRecipients }, - }); - assert.deepStrictEqual(resolveTssVerifyTransactionOptions(txRequest, 'abcd', undefined, 'tsol'), { - txPrebuild: { txHex: 'abcd', consolidateId: 'consolidate-id' }, - txParams: {}, - verification: { consolidationToBaseAddress: true }, - }); - }); - - it('resolves effective txParams for non-consolidation intents', function () { - const txRequest = makeTxRequest({ intent: { intentType: 'payment', recipients: intentRecipients } }); - assert.deepStrictEqual(resolveTssVerifyTransactionOptions(txRequest, 'abcd', undefined, 'tsol'), { - txPrebuild: { txHex: 'abcd' }, - txParams: { recipients: [{ address: 'addr1', amount: '100', data: undefined }], type: 'payment' }, - }); - }); - }); });