diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index d3b4b6643c8..360e15bc54d 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -1146,16 +1146,6 @@ export class EcdsaMPCv2Utils extends BaseEcdsaUtils { params.reqId ); - // Messages are combined and marked delivered by wallet-platform inside - // the final share submission itself (the signed-message Kafka event is - // published there too): by the time this call returns, a message request - // is already terminal. POSTing /send afterwards — as the transaction - // flow does — would be rejected by wallet-platform's pendingDelivery - // assert on the already-delivered request. Re-fetch the request instead, - // mirroring the legacy MPCv1 ECDSA message path. - if (requestType === RequestType.message) { - return getTxRequest(this.bitgo, txRequest.walletId, txRequest.txRequestId, params.reqId); - } return sendTxRequest(this.bitgo, txRequest.walletId, txRequest.txRequestId, requestType, params.reqId); } diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts index aa10deb45d6..4068a064526 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -22,10 +22,8 @@ import { SignatureShareRecord, SignatureShareType, TxRequest, - RequestTracer, } from '../../../../../../src'; import { bitgoGpgKey } from './gpgKeys'; -import * as openpgp from 'openpgp'; describe('ECDSA MPC v2', async () => { let userShare: Buffer; @@ -1384,206 +1382,3 @@ describe('EcdsaMPCv2Utils.createKeychainsWithExternalSigner', function () { ); }); }); - -describe('signRequestBase message finalization', () => { - let sandbox: sinon.SinonSandbox; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - const walletID = '62fe536a6b4cf70007acb48c0e7bb0b0'; - const fakeReqId = { inc: sinon.stub(), toString: sinon.stub().returns('test-req') } as unknown as RequestTracer; - - let userShare: Buffer; - let bitgoKeyShare: Buffer; - before('generate key shares for finalization tests', async () => { - const [userDkgSession, , bitgoDkgSession] = await DklsUtils.generateDKGKeyShares(); - userShare = userDkgSession.getKeyShare(); - bitgoKeyShare = bitgoDkgSession.getKeyShare(); - }); - - /** - * Drives signRequestBase through the REAL DKLS protocol: the user-side DSG - * session is the one the class builds itself; the BitGo-side session is a - * real DSG advanced by this test's mocked HTTP layer on every /sign POST, - * using the signBitgoMPCv2RoundX helpers. Only the HTTP transport is faked - * (bitgo.get / bitgo.post chains), which is exactly the surface the - * finalization-tail decision acts on: GET txrequests vs POST /send. - */ - const runFullSigningFlow = async (params: { requestType: 'tx' | 'message'; txRequest: TxRequest }) => { - const postedUrls: string[] = []; - const getUrls: string[] = []; - const hashBuffer = Buffer.alloc(32, 1); - const derivationPath = 'm/0'; - - // The BitGo-side session signs the same hash buffer the user session is - // given via the getMpcv2HashBuffer stub below. - const bitgoSession = new DklsDsg.Dsg(bitgoKeyShare, 2, derivationPath, hashBuffer); - - // Carrier shaped for the signBitgoMPCv2RoundX helpers, which index - // transactions[0]; the response is remapped back to the real resource. - const carrier = { transactions: [{ signatureShares: [] }] } as unknown as TxRequest; - let signPostCount = 0; - - const mockBg = { - getEnv: () => 'test', - setRequestTracer: () => undefined, - url: (p: string) => p, - get: (url: string) => ({ - query() { - return this; - }, - retry() { - return this; - }, - async result() { - getUrls.push(url); - return { - txRequests: [ - { - ...params.txRequest, - state: 'delivered', - [params.requestType === 'message' ? 'messages' : 'transactions']: [ - { - state: 'signed', - txHash: '0xsigned-message-hash', - signatureShares: [], - ...(params.requestType === 'tx' - ? { unsignedTx: { derivationPath, signableHex: 'abcd', serializedTxHex: 'abcd' } } - : {}), - }, - ], - }, - ], - }; - }, - }), - post: (url: string) => { - postedUrls.push(url); - return { - _body: undefined as unknown, - send(body?: unknown) { - this._body = body; - return this; - }, - async result() { - if (!url.endsWith('/sign')) { - return { ...params.txRequest, state: 'delivered' }; - } - signPostCount += 1; - const body = this._body as { signatureShares: SignatureShareRecord[]; signerGpgPublicKey: string }; - const userShareRecord = body.signatureShares[0]; - if (signPostCount === 1) { - await signBitgoMPCv2Round1(bitgoSession, carrier, userShareRecord, body.signerGpgPublicKey); - } else if (signPostCount === 2) { - await signBitgoMPCv2Round2(bitgoSession, carrier, userShareRecord, body.signerGpgPublicKey); - } else { - await signBitgoMPCv2Round3(bitgoSession, userShareRecord, body.signerGpgPublicKey); - } - const shares = carrier.transactions![0].signatureShares; - return { - ...params.txRequest, - [params.requestType === 'message' ? 'messages' : 'transactions']: [{ signatureShares: shares }], - } as unknown as TxRequest; - }, - }; - }, - } as unknown as BitGoBase; - - const mockCoin = { - getMPCAlgorithm: () => 'ecdsa', - verifyTransaction: () => Promise.resolve(true), - getConfig: () => ({ family: 'eth' }), - } as unknown as IBaseCoin; - const mockWallet = { - id: () => walletID, - multisigType: () => 'tss', - multisigTypeVersion: () => 'MPCv2', - }; - const utils = new EcdsaMPCv2Utils(mockBg, mockCoin, mockWallet as never); - sandbox - .stub(utils as unknown as Record, 'pickBitgoPubGpgKeyForSigning') - .resolves(await openpgp.readKey({ armoredKey: bitgoGpgKey.public })); - sandbox.stub(utils as unknown as Record, 'getMpcv2HashBuffer').returns(hashBuffer); - - const baseParams = { - txRequest: params.txRequest, - prv: userShare.toString('base64'), - reqId: fakeReqId, - }; - const result = - params.requestType === 'message' - ? await utils.signTxRequestForMessage({ - ...baseParams, - messageRaw: 'raw', - messageEncoded: 'abcd', - bufferToSign: Buffer.from('abcd', 'hex'), - }) - : await utils.signTxRequest({ - ...baseParams, - txParams: { recipients: [{ address: '0x' + '00'.repeat(20), amount: '1000' }] }, - }); - - return { result, postedUrls, getUrls }; - }; - - it('message requests finalize via GET (wallet-platform delivers at share ingestion), never POST /send', async () => { - const txRequestId = 'msg-txreq-finalization'; - const { result, postedUrls, getUrls } = await runFullSigningFlow({ - requestType: 'message', - txRequest: { - txRequestId, - walletId: walletID, - apiVersion: 'full', - state: 'pendingDelivery', - messages: [{ messageRaw: 'raw', messageEncoded: 'abcd', derivationPath: 'm/0' }], - } as unknown as TxRequest, - }); - - assert.strictEqual(result.state, 'delivered'); - assert.strictEqual( - postedUrls.filter((url) => url.endsWith('/send')).length, - 0, - 'message signing must not POST /send (wallet-platform already delivered at share ingestion)' - ); - assert.ok( - postedUrls.every((url) => url.includes(`/txrequests/${txRequestId}/messages/0/sign`)), - 'all message POSTs must be share submissions' - ); - // The park-check GET is skipped (object txRequest) so the only GET is the - // final re-fetch. - assert.strictEqual(getUrls.length, 1); - assert.ok(getUrls[0].includes('/txrequests')); - }); - - it('transaction requests still finalize via POST /send', async () => { - const txRequestId = 'tx-txreq-finalization'; - const { result, postedUrls } = await runFullSigningFlow({ - requestType: 'tx', - txRequest: { - txRequestId, - walletId: walletID, - apiVersion: 'full', - state: 'pendingDelivery', - transactions: [ - { - unsignedTx: { derivationPath: 'm/0', signableHex: 'abcd', serializedTxHex: 'abcd' }, - signatureShares: [], - }, - ], - } as unknown as TxRequest, - }); - - assert.strictEqual(result.state, 'delivered'); - assert.strictEqual( - postedUrls.filter((url) => url.endsWith('/transactions/0/send')).length, - 1, - 'transaction signing must POST /send to broadcast' - ); - }); -});