From 8f415331d664b0afc692fb2abefac2a95ceaa751 Mon Sep 17 00:00:00 2001 From: Marzooqa Naeema Kather Date: Thu, 23 Apr 2026 21:11:22 +0530 Subject: [PATCH] fix: add EdDSA MPCv2 chain code support for address derivation - Bump @bitgo/wasm-mps to 1.7.0 which exposes chaincode on the Share struct - Add shareChaincode field and getCommonKeychain() to DKG class in sdk-lib-mpc, returning pubkey+chaincode (128 hex chars) matching Eddsa.deriveUnhardened expectations - Use getCommonKeychain() in EddsaMPCv2Utils.createKeychains and pass full keychain to user/backup/bitgo keychains instead of just the 32-byte public key - Rename commonPublicKey to commonPublicKeychain in round 2 response handling to align with updated @bitgo/public-types and HSM response field name - Remove 64-char MPCv2 special case from getPublicKeyFromCommonKeychain; all EdDSA keychains are now uniformly 128 hex chars (pubkey + chaincode) - Update tests accordingly Co-Authored-By: Claude Sonnet 4.6 TICKET: WCI-230 --- .../test/v2/unit/internal/tssUtils/eddsa.ts | 22 +++++++---------- .../tssUtils/eddsaMPCv2/createKeychains.ts | 13 +++++----- .../src/bitgo/utils/tss/eddsa/base.ts | 11 +++------ .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 24 ++++++++++++------- modules/sdk-lib-mpc/package.json | 2 +- modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts | 16 +++++++++++-- yarn.lock | 8 +++---- 7 files changed, 53 insertions(+), 43 deletions(-) diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts index ca077e9cd2..6100856062 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts @@ -1216,27 +1216,21 @@ describe('TSS Utils:', async function () { } describe('getPublicKeyFromCommonKeychain', function () { - // 32-byte ed25519 public key as hex (64 chars) — the format produced by DKG getSharePublicKey().toString('hex') - const mpcv2CommonKeychain = 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270'; - // MPCv1 appends a 32-byte chaincode after the public key + // 32-byte ed25519 public key as hex (64 chars) + 32-byte chaincode (64 chars) = 128 chars + const pubHex = 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270'; const chaincode = '9d91c2e6353202cf61f8f275158b3468e9a00f7872fc2fd310b72cd026e2e2f9'; - const mpcv1CommonKeychain = mpcv2CommonKeychain + chaincode; + const commonKeychain = pubHex + chaincode; - it('should decode to the same 32-byte public key for both MPCv1 (128 chars) and MPCv2 (64 chars)', function () { - mpcv1CommonKeychain.length.should.equal(128); - mpcv2CommonKeychain.length.should.equal(64); - - const v1Result = TssUtils.getPublicKeyFromCommonKeychain(mpcv1CommonKeychain); - const v2Result = TssUtils.getPublicKeyFromCommonKeychain(mpcv2CommonKeychain); - - v1Result.should.equal(v2Result); - v1Result.should.equal('ByMPeVxs7e8zGecu8n1M43Mq9qkxBSypNNwHeEu2N6vb'); + it('should decode the 32-byte public key from a 128-char commonKeychain', function () { + commonKeychain.length.should.equal(128); + const result = TssUtils.getPublicKeyFromCommonKeychain(commonKeychain); + result.should.equal('ByMPeVxs7e8zGecu8n1M43Mq9qkxBSypNNwHeEu2N6vb'); }); it('should throw for an invalid commonKeychain length', function () { should.throws( () => TssUtils.getPublicKeyFromCommonKeychain('abcd'), - /Invalid commonKeychain length, expected 64 \(MPCv2\) or 128 \(MPCv1\), got 4/ + /Invalid commonKeychain length, expected 128, got 4/ ); }); }); diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts index 6bcc52017a..6cb2f33216 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts @@ -201,7 +201,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () { .once() .reply(200, { sessionId: 'test-session-id', - commonPublicKey: 'a'.repeat(64), + commonPublicKeychain: 'a'.repeat(128), bitgoMsg2: { message: Buffer.from('garbage').toString('base64'), signature: '-----BEGIN PGP SIGNATURE-----\nFAKE\n-----END PGP SIGNATURE-----', @@ -221,7 +221,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () { .once() .reply(200, { sessionId: 'different-session-id', - commonPublicKey: 'a'.repeat(64), + commonPublicKeychain: 'a'.repeat(128), bitgoMsg2: { message: '', signature: '' }, }); @@ -231,7 +231,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () { ); }); - it('should reject when commonPublicKey from BitGo does not match the locally computed key', async function () { + it('should reject when commonPublicKeychain from BitGo does not match the locally computed keychain', async function () { const bitgoSession = new EddsaMPSDkg.DKG(3, 2, 2); const bitgoState: { msg2?: MPSTypes.DeserializedMessage } = {}; await nockMPSKeyGenRound1(bitgoSession, bitgoState, 1); @@ -255,14 +255,14 @@ describe('TSS EdDSA MPCv2 Utils:', async function () { return { sessionId: 'test-session-id', - commonPublicKey: 'fakefakeee'.repeat(8), // mutated — will not match user/backup computed key + commonPublicKeychain: 'fakefakeee'.repeat(16), // mutated — will not match user/backup computed keychain bitgoMsg2: await MPSComms.detachSignMpsMessage(Buffer.from(bitgoState.msg2.payload), bitgoPrvKeyObj), }; }); await assert.rejects( tssUtils.createKeychains({ passphrase: 'test', enterprise: enterpriseId }), - /does not match BitGo common public key/ + /does not match BitGo common keychain/ ); }); @@ -286,6 +286,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () { name: 'irrelevant', publicKey: bitgoGpgKeyPair.publicKey, mpcv2PublicKey: bitgoGpgKeyPair.publicKey, + eddsaMpcv2PublicKey: bitgoGpgKeyPair.publicKey, enterpriseId, }); nock(stagingBgUrl).get('/api/v1/client/constants').reply(200, { ttl: 3600, constants }); @@ -390,7 +391,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () { return { sessionId, - commonPublicKey: bitgoSession.getSharePublicKey().toString('hex'), + commonPublicKeychain: bitgoSession.getCommonKeychain(), bitgoMsg2: await MPSComms.detachSignMpsMessage(Buffer.from(bitgoState.msg2.payload), bitgoPrvKeyObj), }; }); diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/base.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/base.ts index 5f4561a216..9234f8302f 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/base.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/base.ts @@ -14,20 +14,15 @@ export class BaseEddsaUtils extends baseTSSUtils { /** * Get the commonPub portion of an EdDSA commonKeychain. * - * MPCv1 keychains are 128 hex chars (32-byte public key + 32-byte chaincode). - * MPCv2 keychains are 64 hex chars (32-byte public key only — no chaincode). + * Keychains are 128 hex chars: 32-byte public key + 32-byte chaincode. * * @param {string} commonKeychain * @returns {string} base58-encoded public key */ static getPublicKeyFromCommonKeychain(commonKeychain: string): string { - if (commonKeychain.length !== 64 && commonKeychain.length !== 128) { - throw new Error( - `Invalid commonKeychain length, expected 64 (MPCv2) or 128 (MPCv1), got ${commonKeychain.length}` - ); + if (commonKeychain.length !== 128) { + throw new Error(`Invalid commonKeychain length, expected 128, got ${commonKeychain.length}`); } - // For MPCv1 (128 chars): the first 64 hex chars are the 32-byte public key. - // For MPCv2 (64 chars): the entire string is the 32-byte public key. const pubHex = commonKeychain.slice(0, 64); return bs58.encode(Buffer.from(pubHex, 'hex')); } 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 f4680fc4a0..2e98b8975d 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -97,7 +97,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { const { sessionId: sessionIdRound2, - commonPublicKeychain: commonPublicKey, + commonPublicKeychain, bitgoMsg2, } = await this.sendKeyGenerationRound2(params.enterprise, { sessionId, @@ -123,11 +123,19 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { assert(userFinalMsgs.length === 0, 'WASM round 2 should produce no output messages for user'); assert(backupFinalMsgs.length === 0, 'WASM round 2 should produce no output messages for backup'); - const userCommonKey = userDkg.getSharePublicKey().toString('hex'); - const backupCommonKey = backupDkg.getSharePublicKey().toString('hex'); + const userCommonKeychain = userDkg.getCommonKeychain(); + const backupCommonKeychain = backupDkg.getCommonKeychain(); - assert.equal(userCommonKey, commonPublicKey, 'User computed public key does not match BitGo common public key'); - assert.equal(backupCommonKey, commonPublicKey, 'Backup computed public key does not match BitGo common public key'); + assert.equal( + userCommonKeychain, + commonPublicKeychain, + 'User computed keychain does not match BitGo common keychain' + ); + assert.equal( + backupCommonKeychain, + commonPublicKeychain, + 'Backup computed keychain does not match BitGo common keychain' + ); const userPrivateMaterial = userDkg.getKeyShare(); const backupPrivateMaterial = backupDkg.getKeyShare(); @@ -135,20 +143,20 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { const backupReducedPrivateMaterial = backupDkg.getReducedKeyShare(); const userKeychainPromise = this.addUserKeychain( - commonPublicKey, + userCommonKeychain, userPrivateMaterial, userReducedPrivateMaterial, params.passphrase, params.originalPasscodeEncryptionCode ); const backupKeychainPromise = this.addBackupKeychain( - commonPublicKey, + backupCommonKeychain, backupPrivateMaterial, backupReducedPrivateMaterial, params.passphrase, params.originalPasscodeEncryptionCode ); - const bitgoKeychainPromise = this.addBitgoKeychain(commonPublicKey); + const bitgoKeychainPromise = this.addBitgoKeychain(userCommonKeychain); const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([ userKeychainPromise, diff --git a/modules/sdk-lib-mpc/package.json b/modules/sdk-lib-mpc/package.json index d4047a940c..6bf73ee07b 100644 --- a/modules/sdk-lib-mpc/package.json +++ b/modules/sdk-lib-mpc/package.json @@ -36,7 +36,7 @@ ] }, "dependencies": { - "@bitgo/wasm-mps": "1.6.0", + "@bitgo/wasm-mps": "1.7.0", "@noble/curves": "1.8.1", "@silencelaboratories/dkls-wasm-ll-node": "1.2.0-pre.4", "@silencelaboratories/dkls-wasm-ll-web": "1.2.0-pre.4", 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 cad1ce6e71..4f9b95eaf2 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts @@ -36,6 +36,8 @@ export class DKG { private keyShare: Buffer | null = null; /** 32-byte Ed25519 public key from round2 */ private sharePk: Buffer | null = null; + /** 32-byte chain code from round2 */ + private shareChaincode: Buffer | null = null; protected dkgState: DkgState = DkgState.Uninitialized; @@ -153,6 +155,7 @@ export class DKG { } this.keyShare = Buffer.from(share.share); this.sharePk = Buffer.from(share.pk); + this.shareChaincode = Buffer.from(share.chaincode); this.dkgStateBytes = null; this.dkgState = DkgState.Complete; return []; @@ -182,10 +185,19 @@ export class DKG { return this.sharePk; } + /** + * Returns the 128-char hex common keychain: 64-char public key + 64-char chain code. + * This matches the format expected by address derivation (Eddsa.deriveUnhardened). + */ + getCommonKeychain(): string { + if (!this.sharePk || !this.shareChaincode) { + throw Error('DKG session not initialized'); + } + return this.sharePk.toString('hex') + this.shareChaincode.toString('hex'); + } + /** * Returns a CBOR-encoded reduced representation containing the public key. - * Note: private key material and chain code are not separately accessible - * from @bitgo/wasm-mps; the full keyshare is available via getKeyShare(). */ getReducedKeyShare(): Buffer { if (!this.sharePk) { diff --git a/yarn.lock b/yarn.lock index f6c89b0d43..006ad9082b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1015,10 +1015,10 @@ resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.7.0.tgz" integrity sha512-KoXavJvyDHlEN+sWcigbgxYJtdFaU7gS0EkYQbNH4npVjNlzo6rL6gwjyWbyOy7oEs65DhpJ9vY5kRbE/bKiTQ== -"@bitgo/wasm-mps@1.6.0": - version "1.6.0" - resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.6.0.tgz#3e1f0618c1efac35ccd56301f8198f19d934e5ed" - integrity sha512-4Mzs124Wj3QbqaZqTYX4t2vSVNKblL/53SQFddoPgggfCnZpuV4tYovpD2sIwhbWe8hVWJXZR2/1CP+zUHKMaw== +"@bitgo/wasm-mps@1.7.0": + version "1.7.0" + resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.7.0.tgz#e7ebca1afd2df757e69c5cdac702d6a06156867c" + integrity sha512-SNO7as4UvnE2ptDXp1oUXjABA8Y3/71lgVpAQyAGSfSaURjz4rG19+JZR54GBRIaA6hvUPr029b4gFyqoZPcgg== "@bitgo/wasm-solana@^2.6.0": version "2.6.0"