From c49bc247424de6a75e29b437754661c0b7e37cfc Mon Sep 17 00:00:00 2001 From: Vibhav Simha G Date: Fri, 4 Sep 2026 15:20:40 +0530 Subject: [PATCH] fix(sdk-coin-polyx): transferAllowDeath in getFee and remove double Ed25519 prefix Two bugs that together block POLYX MPCv2 recovery end-to-end: 1. getFee used api.tx.balances.transfer which was renamed transferAllowDeath in Polymesh v8 (same call slot 0x0500). Blocked recovery at fee estimation with TypeError before any transaction was produced. 2. MPCv2 signing path manually prepended the Ed25519 MultiSignature discriminant (0x00) to rawSig before addSignature, but Transaction#constructSignedPayload already prepends that byte. The double prefix shifted the on-wire signature by one byte and dropped the last byte of sigma, causing 1010: Bad signature. Mirrors the fix applied to abstractSubstrateCoin in WCI-1454. Ticket: WCI-1437 --- modules/sdk-coin-polyx/src/polyx.ts | 8 ++----- modules/sdk-coin-polyx/test/unit/polyx.ts | 26 ++++++++++++++++++++--- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/modules/sdk-coin-polyx/src/polyx.ts b/modules/sdk-coin-polyx/src/polyx.ts index ff1d0c1765..a72bcad729 100644 --- a/modules/sdk-coin-polyx/src/polyx.ts +++ b/modules/sdk-coin-polyx/src/polyx.ts @@ -108,7 +108,7 @@ export class Polyx extends SubstrateCoin { protected async getFee(destAddr: string, srcAddr: string, amount: number): Promise { const api = await this.getInitializedNodeAPI(); - const info = await api.tx.balances.transfer(destAddr, amount).paymentInfo(srcAddr); + const info = await api.tx.balances.transferAllowDeath(destAddr, amount).paymentInfo(srcAddr); return info.partialFee.toNumber(); } @@ -195,7 +195,6 @@ export class Polyx extends SubstrateCoin { assert(params.walletPassphrase, 'missing wallet passphrase'); const signingMaterial = await this.getEddsaSigningMaterial(params.userKey, params.walletPassphrase); - const ED25519_PREFIX = 0x00; const substrateKeyPair = new SubstrateKeyPair({ pub: accountId }); if (signingMaterial.version === 'v2') { const rawSig = await this.signSubstrateMpcV2Recovery({ @@ -207,10 +206,7 @@ export class Polyx extends SubstrateCoin { derivationPath: currPath, bitgo: this.bitgo, }); - txBuilder.addSignature( - { pub: substrateKeyPair.getKeys().pub }, - Buffer.concat([Buffer.from([ED25519_PREFIX]), rawSig]) - ); + txBuilder.addSignature({ pub: substrateKeyPair.getKeys().pub }, rawSig); } else { const userSigningMaterial = JSON.parse(signingMaterial.userPrv) as EDDSAMethodTypes.UserSigningMaterial; const backupPrv = await this.bitgo.decrypt({ diff --git a/modules/sdk-coin-polyx/test/unit/polyx.ts b/modules/sdk-coin-polyx/test/unit/polyx.ts index 284c0db38b..3460abc6cb 100644 --- a/modules/sdk-coin-polyx/test/unit/polyx.ts +++ b/modules/sdk-coin-polyx/test/unit/polyx.ts @@ -234,11 +234,11 @@ describe('Polyx:', function () { (result.serializedTx as string).should.be.a.String().and.not.be.empty(); sandBox.assert.notCalled(getTSSSignatureSpy); - // Substrate MultiSignature Ed25519 discriminant (0x00) must prefix the 64-byte signature. sandBox.assert.calledOnce(addSignatureSpy); const signature: Buffer = addSignatureSpy.firstCall.args[1]; - signature.length.should.equal(65); - signature[0].should.equal(0x00); + // constructSignedPayload prepends the 0x00 Ed25519 discriminant internally; + // recover() must pass the raw 64-byte signature unchanged. + signature.length.should.equal(64); }); it('should produce a cryptographically valid Ed25519 signature', async function () { @@ -331,6 +331,26 @@ describe('Polyx:', function () { const paramsWithoutPassphrase = { ...mpcV2RecoverParams, walletPassphrase: undefined }; await baseCoin.recover(paramsWithoutPassphrase).should.be.rejectedWith('missing wallet passphrase'); }); + + it('should pass the raw 64-byte signature to addSignature on MPCv2 path', async function () { + // Regression: previously wrapped rawSig with a manual Ed25519 discriminant (0x00) + // before addSignature. constructSignedPayload already prepends that discriminant, + // so wrapping here caused a double prefix that shifted the on-wire signature by + // one byte, dropping the last byte of sigma and producing + // `1010: Bad signature` on-chain. + const rawSig = Buffer.alloc(64, 0xab); + sandBox + .stub(baseCoin as unknown as { signSubstrateMpcV2Recovery: unknown }, 'signSubstrateMpcV2Recovery') + .resolves(rawSig); + const addSignatureSpy = sandBox.spy(TransferBuilder.prototype, 'addSignature'); + + await baseCoin.recover(mpcV2RecoverParams); + + sandBox.assert.calledOnce(addSignatureSpy); + const sig: Buffer = addSignatureSpy.firstCall.args[1]; + sig.length.should.equal(64); + sig.should.deepEqual(rawSig); + }); }); });