From 92be23fc7641053b41ee36f2b68f8af5d2f98eb5 Mon Sep 17 00:00:00 2001 From: Marzooqa Naeema Kather Date: Tue, 12 May 2026 17:40:16 +0530 Subject: [PATCH] fix(sdk-lib-mpc): add keyShare to EdDSA MPCv2 getReducedKeyShare getReducedKeyShare() was only serialising sharePk (the 32-byte public key) into the CBOR keycard payload, silently discarding the opaque WASM signing key share and chain code. Recovery signing via DSG requires the full keyShare buffer as input to ed25519_dsg_round0_process, making the old format unusable for SDK-local hot-wallet recovery. ReducedKeyShareType now includes keyShare, pub, and rootChainCode, matching the pattern of ECDSA MPCv2's ReducedKeyShare. Tests updated to assert all three fields are present and correct. Ticket: WCI-385 --- modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts | 8 +++-- .../sdk-lib-mpc/src/tss/eddsa-mps/types.ts | 2 ++ .../sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts | 32 +++++++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts index d59e047235..ff16e01be0 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts @@ -234,14 +234,18 @@ export class DKG { } /** - * Returns a CBOR-encoded reduced representation containing the public key. + * Returns a CBOR-encoded ReducedKeyShare buffer containing the party's opaque + * signing key share in the `keyShare` field. This buffer is private key material. + * The caller encrypts it and stores it as `reducedEncryptedPrv` on the key card QR code. */ getReducedKeyShare(): Buffer { - if (!this.sharePk) { + if (!this.keyShare || !this.sharePk || !this.shareChaincode) { throw Error('DKG session not initialized'); } const reducedKeyShare: EddsaReducedKeyShare = { + keyShare: Array.from(this.keyShare), pub: Array.from(this.sharePk), + rootChainCode: Array.from(this.shareChaincode), }; return Buffer.from(encode(reducedKeyShare)); } diff --git a/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts b/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts index f026f322f1..02febddce3 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts @@ -3,7 +3,9 @@ import { isLeft } from 'fp-ts/Either'; import * as t from 'io-ts'; export const ReducedKeyShareType = t.type({ + keyShare: t.array(t.number), pub: t.array(t.number), + rootChainCode: t.array(t.number), }); export type EddsaReducedKeyShare = t.TypeOf; diff --git a/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts index 1f458298b8..f826536fec 100644 --- a/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts +++ b/modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts @@ -205,12 +205,38 @@ describe('EdDSA MPS DKG', function () { ); assert(Buffer.isBuffer(bitgoReduced) && bitgoReduced.length > 0, 'BitGo reduced key share should be non-empty'); - const userPub = Buffer.from(MPSTypes.getDecodedReducedKeyShare(userReduced).pub).toString('hex'); - const backupPub = Buffer.from(MPSTypes.getDecodedReducedKeyShare(backupReduced).pub).toString('hex'); - const bitgoPub = Buffer.from(MPSTypes.getDecodedReducedKeyShare(bitgoReduced).pub).toString('hex'); + const userDecoded = MPSTypes.getDecodedReducedKeyShare(userReduced); + const backupDecoded = MPSTypes.getDecodedReducedKeyShare(backupReduced); + const bitgoDecoded = MPSTypes.getDecodedReducedKeyShare(bitgoReduced); + + const userPub = Buffer.from(userDecoded.pub).toString('hex'); + const backupPub = Buffer.from(backupDecoded.pub).toString('hex'); + const bitgoPub = Buffer.from(bitgoDecoded.pub).toString('hex'); assert.strictEqual(userPub, backupPub, 'User and backup should have same public key in reduced share'); assert.strictEqual(backupPub, bitgoPub, 'Backup and BitGo should have same public key in reduced share'); + + // keyShare must be present and non-empty (opaque WASM bincode needed for DSG) + assert(userDecoded.keyShare.length > 0, 'User reduced share must include keyShare'); + assert(backupDecoded.keyShare.length > 0, 'Backup reduced share must include keyShare'); + assert(bitgoDecoded.keyShare.length > 0, 'BitGo reduced share must include keyShare'); + + // rootChainCode must be 32 bytes + assert.strictEqual(userDecoded.rootChainCode.length, 32, 'User rootChainCode must be 32 bytes'); + assert.strictEqual(backupDecoded.rootChainCode.length, 32, 'Backup rootChainCode must be 32 bytes'); + assert.strictEqual(bitgoDecoded.rootChainCode.length, 32, 'BitGo rootChainCode must be 32 bytes'); + + // All parties derive the same chaincode + assert.strictEqual( + Buffer.from(userDecoded.rootChainCode).toString('hex'), + Buffer.from(backupDecoded.rootChainCode).toString('hex'), + 'User and backup should have same rootChainCode' + ); + assert.strictEqual( + Buffer.from(backupDecoded.rootChainCode).toString('hex'), + Buffer.from(bitgoDecoded.rootChainCode).toString('hex'), + 'Backup and BitGo should have same rootChainCode' + ); }); });