From 4af287de2845b8d20453f99a2b8dc0de64d42aff Mon Sep 17 00:00:00 2001 From: Marzooqa Kather Date: Mon, 10 Aug 2026 16:11:12 +0000 Subject: [PATCH 1/2] feat(sdk-core): add getMpcV2RetrofitDataFromMpcV1Keys for EdDSA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `getMpcV2RetrofitDataFromMpcV1Keys` (public instance method) and a private static helper `getMpcV2RetrofitDataFromMpcV1Key` on `EddsaMPCv2Utils`. Also introduce the `EddsaRetrofitData` type and export it from the package root. The methods convert MPCv1 EdDSA key shares (JSON with uShare.seed and uShare.chaincode) into `EddsaRetrofitData` objects that can be passed to `new EddsaMPSDkg.DKG(…, retrofitData)` to seed a retrofit DKG ceremony. Scalar derivation follows RFC 8032 Ed25519 key expansion: scalar = clamp(SHA512(seed_bytes)[0:32]) where clamping clears bits 0-2 of byte[0], clears bit 7 of byte[31], and sets bit 6 of byte[31]. The aggregate public key (expectedPk) is read from the user key's pShare.y and reused for both parties, since both parties share the same root public key in the MPCv1 scheme. Required by WCI-1264 (getUserAndBackupSession + createKeychains retrofit wiring) to derive the retrofit data before starting the DKG ceremony. Ticket: WCI-1263 Session-Id: 9430179b-3b09-4c4c-9c75-723c484f16b5 Task-Id: 29bdbf4d-6e4f-4646-bd79-06a89e5dcf28 --- .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 50 +++++++ modules/sdk-core/src/index.ts | 4 +- .../unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 135 ++++++++++++++++++ 3 files changed, 187 insertions(+), 2 deletions(-) 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 9613906817..f082d179ad 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -1,4 +1,5 @@ import assert from 'assert'; +import crypto from 'crypto'; import * as pgp from 'openpgp'; import * as sjcl from '@bitgo/sjcl'; import { NonEmptyString } from 'io-ts-types'; @@ -51,6 +52,12 @@ import { resolveEffectiveTxParams } from '../recipientUtils'; import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './eddsaMPCv2KeyGenSender'; import { EddsaMPCv2RecoveryKeyShares } from './types'; +export type EddsaRetrofitData = { + s_i_0: string; + expectedPk: string; + chainCode: string; +}; + export class EddsaMPCv2Utils extends BaseEddsaUtils { private static readonly MPS_DSG_SIGNING_USER_GPG_KEY = 'MPS_DSG_SIGNING_USER_GPG_KEY'; private static readonly MPS_DSG_SIGNING_ROUND1_STATE = 'MPS_DSG_SIGNING_ROUND1_STATE'; @@ -1066,6 +1073,49 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { return sendTxRequest(this.bitgo, txRequestResolved.walletId, txRequestResolved.txRequestId, requestType, reqId); } // #endregion + + // #region retrofit + + getMpcV2RetrofitDataFromMpcV1Keys(params: { + mpcv1UserKeyShare: string; + mpcv1BackupKeyShare: string; + }): { + userRetrofitData: EddsaRetrofitData; + backupRetrofitData: EddsaRetrofitData; + } { + const userKey = JSON.parse(params.mpcv1UserKeyShare); + assert(typeof userKey?.pShare?.y === 'string', 'MPCv1 user key missing pShare.y (aggregate public key)'); + const expectedPk: string = userKey.pShare.y; + + return { + userRetrofitData: EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Key(params.mpcv1UserKeyShare, expectedPk), + backupRetrofitData: EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Key(params.mpcv1BackupKeyShare, expectedPk), + }; + } + + private static getMpcV2RetrofitDataFromMpcV1Key( + decryptedKeyShare: string, + expectedPk: string + ): EddsaRetrofitData { + const key = JSON.parse(decryptedKeyShare); + assert(typeof key?.uShare?.seed === 'string', 'MPCv1 key missing uShare.seed'); + assert(typeof key?.uShare?.chaincode === 'string', 'MPCv1 key missing uShare.chaincode'); + + const seedBytes = Buffer.from(key.uShare.seed, 'hex'); + const hash = crypto.createHash('sha512').update(seedBytes).digest(); + const scalar = Buffer.from(hash.subarray(0, 32)); + scalar[0] &= 248; + scalar[31] &= 127; + scalar[31] |= 64; + + return { + s_i_0: scalar.toString('hex'), + expectedPk, + chainCode: key.uShare.chaincode, + }; + } + + // #endregion } /** diff --git a/modules/sdk-core/src/index.ts b/modules/sdk-core/src/index.ts index 94783ed2ed..a6ee46bc1d 100644 --- a/modules/sdk-core/src/index.ts +++ b/modules/sdk-core/src/index.ts @@ -29,8 +29,8 @@ import { EcdsaUtils } from './bitgo/utils/tss/ecdsa/ecdsa'; export { EcdsaUtils }; import { EcdsaMPCv2Utils } from './bitgo/utils/tss/ecdsa/ecdsaMPCv2'; export { EcdsaMPCv2Utils }; -import { EddsaMPCv2Utils } from './bitgo/utils/tss/eddsa/eddsaMPCv2'; -export { EddsaMPCv2Utils }; +import { EddsaMPCv2Utils, EddsaRetrofitData } from './bitgo/utils/tss/eddsa/eddsaMPCv2'; +export { EddsaMPCv2Utils, EddsaRetrofitData }; export { verifyEddsaTssWalletAddress, verifyMPCWalletAddress } from './bitgo/utils/tss/addressVerification'; export { GShare, SignShare, YShare } from './account-lib/mpc/tss/eddsa/types'; export { TssEcdsaStep1ReturnMessage, TssEcdsaStep2ReturnMessage } from './bitgo/tss/types'; diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index dbc8572d92..8d33bf925d 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -22,6 +22,7 @@ import { EDDSAUtils, EddsaMPCv2KeyGenCallbacks, EddsaMPCv2Utils, + EddsaRetrofitData, IBaseCoin, IWallet, RequestTracer, @@ -2178,3 +2179,137 @@ describe('EddsaMPCv2Utils.createKeychainsWithExternalSigner', function () { ); }); }); + +describe('EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Keys', () => { + // 32-byte seed and chaincode values used across all tests + const userSeed = randomBytes(32).toString('hex'); + const backupSeed = randomBytes(32).toString('hex'); + const userChaincode = randomBytes(32).toString('hex'); + const backupChaincode = randomBytes(32).toString('hex'); + const aggregatePk = randomBytes(32).toString('hex'); + + const userMpcV1Key = JSON.stringify({ + uShare: { i: 1, seed: userSeed, chaincode: userChaincode, y: randomBytes(32).toString('hex') }, + pShare: { y: aggregatePk }, + bitgoYShare: { u: randomBytes(32).toString('hex') }, + backupYShare: { u: randomBytes(32).toString('hex') }, + }); + + const backupMpcV1Key = JSON.stringify({ + uShare: { i: 2, seed: backupSeed, chaincode: backupChaincode, y: randomBytes(32).toString('hex') }, + bitgoYShare: { u: randomBytes(32).toString('hex') }, + userYShare: { u: randomBytes(32).toString('hex') }, + }); + + let utils: EddsaMPCv2Utils; + + beforeEach(() => { + const mockBitGo = {} as unknown as BitGoBase; + const mockCoin = {} as unknown as IBaseCoin; + utils = new EddsaMPCv2Utils(mockBitGo, mockCoin); + }); + + function deriveScalar(seedHex: string): string { + const { createHash } = require('crypto'); + const seedBytes = Buffer.from(seedHex, 'hex'); + const hash = createHash('sha512').update(seedBytes).digest(); + const scalar = Buffer.from(hash.subarray(0, 32)); + scalar[0] &= 248; + scalar[31] &= 127; + scalar[31] |= 64; + return scalar.toString('hex'); + } + + it('returns EddsaRetrofitData for user and backup with matching expectedPk', () => { + const result = utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: userMpcV1Key, + mpcv1BackupKeyShare: backupMpcV1Key, + }); + + const { userRetrofitData, backupRetrofitData } = result as { + userRetrofitData: EddsaRetrofitData; + backupRetrofitData: EddsaRetrofitData; + }; + + assert.strictEqual(userRetrofitData.expectedPk, aggregatePk); + assert.strictEqual(backupRetrofitData.expectedPk, aggregatePk); + }); + + it('returns correct chainCode per party', () => { + const { userRetrofitData, backupRetrofitData } = utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: userMpcV1Key, + mpcv1BackupKeyShare: backupMpcV1Key, + }); + + assert.strictEqual(userRetrofitData.chainCode, userChaincode); + assert.strictEqual(backupRetrofitData.chainCode, backupChaincode); + }); + + it('returns correctly clamped s_i_0 scalars', () => { + const { userRetrofitData, backupRetrofitData } = utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: userMpcV1Key, + mpcv1BackupKeyShare: backupMpcV1Key, + }); + + assert.strictEqual(userRetrofitData.s_i_0, deriveScalar(userSeed)); + assert.strictEqual(backupRetrofitData.s_i_0, deriveScalar(backupSeed)); + }); + + it('scalar byte[0] has low 3 bits cleared, byte[31] has bit7 cleared and bit6 set', () => { + const { userRetrofitData } = utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: userMpcV1Key, + mpcv1BackupKeyShare: backupMpcV1Key, + }); + + const scalarBytes = Buffer.from(userRetrofitData.s_i_0, 'hex'); + assert.strictEqual(scalarBytes[0] & 0b111, 0, 'byte[0] low 3 bits should be cleared'); + assert.strictEqual(scalarBytes[31] & 0b10000000, 0, 'byte[31] bit7 should be cleared'); + assert.strictEqual(scalarBytes[31] & 0b01000000, 0b01000000, 'byte[31] bit6 should be set'); + }); + + it('throws if user key is missing pShare.y', () => { + const keyNoPShare = JSON.stringify({ + uShare: { i: 1, seed: userSeed, chaincode: userChaincode }, + bitgoYShare: { u: 'x' }, + }); + assert.throws( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: keyNoPShare, + mpcv1BackupKeyShare: backupMpcV1Key, + }), + /MPCv1 user key missing pShare\.y/ + ); + }); + + it('throws if user key is missing uShare.seed', () => { + const keyNoSeed = JSON.stringify({ + uShare: { i: 1, chaincode: userChaincode }, + pShare: { y: aggregatePk }, + bitgoYShare: { u: 'x' }, + }); + assert.throws( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: keyNoSeed, + mpcv1BackupKeyShare: backupMpcV1Key, + }), + /MPCv1 key missing uShare\.seed/ + ); + }); + + it('throws if backup key is missing uShare.seed', () => { + const backupNoSeed = JSON.stringify({ + uShare: { i: 2, chaincode: backupChaincode }, + bitgoYShare: { u: 'x' }, + }); + assert.throws( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: userMpcV1Key, + mpcv1BackupKeyShare: backupNoSeed, + }), + /MPCv1 key missing uShare\.seed/ + ); + }); +}); From 9957029ed0d4124bab64b932eb0eb0ed1eb35c60 Mon Sep 17 00:00:00 2001 From: Marzooqa Kather Date: Mon, 10 Aug 2026 16:17:07 +0000 Subject: [PATCH 2/2] fix(sdk-core): derive EdDSA retrofit data via real MPCv1 keyCombine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the hand-rolled scalar derivation (manual SHA-512 + clamp with a zeroize try/finally, and reading a non-existent `pShare` field off the raw decrypted keycard JSON) with a call to `MPC.keyCombine(uShare, yShares)` — the same combine step every other MPCv1 EdDSA code path already uses. The decrypted MPCv1 key share (`SigningMaterial`) only ever contains `uShare` + `bitgoYShare` + `backupYShare`/`userYShare`; it has no `pShare`, and `uShare.chaincode` is only one party's additive contribution to the real BIP32 chain code, not the combined value. Deriving `s_i_0`/`expectedPk`/`chainCode` from the real `pShare.u`/`pShare.y`/`pShare.chaincode` output of keyCombine fixes both issues at once and asserts user/backup agree on the aggregate public key and chain code before returning. Structures the per-party helper like ecdsaMPCv2.ts's getMpcV2RetrofitDataFromMpcV1Key: it takes the raw JSON key-share string and an MPCv2PartiesEnum.USER/BACKUP index, JSON.parses and switches on the index internally, and asserts every share it's about to feed into keyCombine (uShare, bitgoYShare, and the counterparty YShare) right where they're used — mirroring ECDSA's assertion style since JSON.parse returns unchecked data and the SigningMaterial type's non-optional fields aren't a runtime guarantee. Moves `EddsaRetrofitData` into `@bitgo/sdk-lib-mpc`'s `tss/eddsa-mps/types.ts` (as `MPSTypes.EddsaRetrofitData`), mirroring where DKLS keeps its `RetrofitData` type, instead of defining and re-exporting it from sdk-core. Rewrites tests to exercise real 3-party MPCv1 key shares generated via `Eddsa.keyShare`/`keyCombine` instead of hand-built JSON fixtures with a `pShare` field that never occurs in production data, and covers each assertion branch. Ticket: WCI-1263 --- .../src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 93 +++++---- modules/sdk-core/src/index.ts | 4 +- .../unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 191 ++++++++++-------- .../sdk-lib-mpc/src/tss/eddsa-mps/types.ts | 14 ++ 4 files changed, 176 insertions(+), 126 deletions(-) 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 f082d179ad..64b4b5df9c 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -1,5 +1,4 @@ import assert from 'assert'; -import crypto from 'crypto'; import * as pgp from 'openpgp'; import * as sjcl from '@bitgo/sjcl'; import { NonEmptyString } from 'io-ts-types'; @@ -51,12 +50,7 @@ import { BaseEddsaUtils } from './base'; import { resolveEffectiveTxParams } from '../recipientUtils'; import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './eddsaMPCv2KeyGenSender'; import { EddsaMPCv2RecoveryKeyShares } from './types'; - -export type EddsaRetrofitData = { - s_i_0: string; - expectedPk: string; - chainCode: string; -}; +import { SigningMaterial } from '../../../tss'; export class EddsaMPCv2Utils extends BaseEddsaUtils { private static readonly MPS_DSG_SIGNING_USER_GPG_KEY = 'MPS_DSG_SIGNING_USER_GPG_KEY'; @@ -1076,42 +1070,67 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { // #region retrofit - getMpcV2RetrofitDataFromMpcV1Keys(params: { - mpcv1UserKeyShare: string; - mpcv1BackupKeyShare: string; - }): { - userRetrofitData: EddsaRetrofitData; - backupRetrofitData: EddsaRetrofitData; - } { - const userKey = JSON.parse(params.mpcv1UserKeyShare); - assert(typeof userKey?.pShare?.y === 'string', 'MPCv1 user key missing pShare.y (aggregate public key)'); - const expectedPk: string = userKey.pShare.y; + async getMpcV2RetrofitDataFromMpcV1Keys(params: { mpcv1UserKeyShare: string; mpcv1BackupKeyShare: string }): Promise<{ + userRetrofitData: MPSTypes.EddsaRetrofitData; + backupRetrofitData: MPSTypes.EddsaRetrofitData; + }> { + const MPC = await getInitializedMpcInstance(); + const userRetrofitData = EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Key( + params.mpcv1UserKeyShare, + MPCv2PartiesEnum.USER, + MPC + ); + const backupRetrofitData = EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Key( + params.mpcv1BackupKeyShare, + MPCv2PartiesEnum.BACKUP, + MPC + ); - return { - userRetrofitData: EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Key(params.mpcv1UserKeyShare, expectedPk), - backupRetrofitData: EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Key(params.mpcv1BackupKeyShare, expectedPk), - }; + assert( + userRetrofitData.expectedPk === backupRetrofitData.expectedPk, + 'MPCv1 user and backup keys combine to different aggregate public keys' + ); + assert( + userRetrofitData.chainCode === backupRetrofitData.chainCode, + 'MPCv1 user and backup keys combine to different chain codes' + ); + + return { userRetrofitData, backupRetrofitData }; } private static getMpcV2RetrofitDataFromMpcV1Key( - decryptedKeyShare: string, - expectedPk: string - ): EddsaRetrofitData { - const key = JSON.parse(decryptedKeyShare); - assert(typeof key?.uShare?.seed === 'string', 'MPCv1 key missing uShare.seed'); - assert(typeof key?.uShare?.chaincode === 'string', 'MPCv1 key missing uShare.chaincode'); - - const seedBytes = Buffer.from(key.uShare.seed, 'hex'); - const hash = crypto.createHash('sha512').update(seedBytes).digest(); - const scalar = Buffer.from(hash.subarray(0, 32)); - scalar[0] &= 248; - scalar[31] &= 127; - scalar[31] |= 64; + mpcv1PartyKeyShare: string, + mpcv1PartyIndex: MPCv2PartiesEnum.USER | MPCv2PartiesEnum.BACKUP, + mpc: Awaited> + ): MPSTypes.EddsaRetrofitData { + const signingMaterial: SigningMaterial = JSON.parse(mpcv1PartyKeyShare); + assert(signingMaterial.uShare, 'MPCv1 key material missing uShare'); + assert(signingMaterial.bitgoYShare, 'MPCv1 key material missing bitgoYShare'); + + let pShare; + switch (mpcv1PartyIndex) { + case MPCv2PartiesEnum.USER: + assert(signingMaterial.backupYShare, 'User MPCv1 key material missing backupYShare'); + pShare = mpc.keyCombine(signingMaterial.uShare, [ + signingMaterial.bitgoYShare, + signingMaterial.backupYShare, + ]).pShare; + break; + case MPCv2PartiesEnum.BACKUP: + assert(signingMaterial.userYShare, 'Backup MPCv1 key material missing userYShare'); + pShare = mpc.keyCombine(signingMaterial.uShare, [ + signingMaterial.bitgoYShare, + signingMaterial.userYShare, + ]).pShare; + break; + default: + throw new Error('Invalid participant index'); + } return { - s_i_0: scalar.toString('hex'), - expectedPk, - chainCode: key.uShare.chaincode, + s_i_0: pShare.u, + expectedPk: pShare.y, + chainCode: pShare.chaincode, }; } diff --git a/modules/sdk-core/src/index.ts b/modules/sdk-core/src/index.ts index a6ee46bc1d..94783ed2ed 100644 --- a/modules/sdk-core/src/index.ts +++ b/modules/sdk-core/src/index.ts @@ -29,8 +29,8 @@ import { EcdsaUtils } from './bitgo/utils/tss/ecdsa/ecdsa'; export { EcdsaUtils }; import { EcdsaMPCv2Utils } from './bitgo/utils/tss/ecdsa/ecdsaMPCv2'; export { EcdsaMPCv2Utils }; -import { EddsaMPCv2Utils, EddsaRetrofitData } from './bitgo/utils/tss/eddsa/eddsaMPCv2'; -export { EddsaMPCv2Utils, EddsaRetrofitData }; +import { EddsaMPCv2Utils } from './bitgo/utils/tss/eddsa/eddsaMPCv2'; +export { EddsaMPCv2Utils }; export { verifyEddsaTssWalletAddress, verifyMPCWalletAddress } from './bitgo/utils/tss/addressVerification'; export { GShare, SignShare, YShare } from './account-lib/mpc/tss/eddsa/types'; export { TssEcdsaStep1ReturnMessage, TssEcdsaStep2ReturnMessage } from './bitgo/tss/types'; diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index 8d33bf925d..842bbdaa93 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -22,7 +22,6 @@ import { EDDSAUtils, EddsaMPCv2KeyGenCallbacks, EddsaMPCv2Utils, - EddsaRetrofitData, IBaseCoin, IWallet, RequestTracer, @@ -2181,135 +2180,153 @@ describe('EddsaMPCv2Utils.createKeychainsWithExternalSigner', function () { }); describe('EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Keys', () => { - // 32-byte seed and chaincode values used across all tests - const userSeed = randomBytes(32).toString('hex'); - const backupSeed = randomBytes(32).toString('hex'); - const userChaincode = randomBytes(32).toString('hex'); - const backupChaincode = randomBytes(32).toString('hex'); - const aggregatePk = randomBytes(32).toString('hex'); - - const userMpcV1Key = JSON.stringify({ - uShare: { i: 1, seed: userSeed, chaincode: userChaincode, y: randomBytes(32).toString('hex') }, - pShare: { y: aggregatePk }, - bitgoYShare: { u: randomBytes(32).toString('hex') }, - backupYShare: { u: randomBytes(32).toString('hex') }, - }); + let utils: EddsaMPCv2Utils; + // Real 3-party MPCv1 EdDSA key shares: 1 = user, 2 = backup, 3 = bitgo. + let userSigningMaterial: Record; + let backupSigningMaterial: Record; + let expectedUserPShare: { y: string; u: string; chaincode: string }; + let expectedBackupPShare: { y: string; u: string; chaincode: string }; - const backupMpcV1Key = JSON.stringify({ - uShare: { i: 2, seed: backupSeed, chaincode: backupChaincode, y: randomBytes(32).toString('hex') }, - bitgoYShare: { u: randomBytes(32).toString('hex') }, - userYShare: { u: randomBytes(32).toString('hex') }, + before(async () => { + const MPC = await getInitializedMpcInstance(); + const user = MPC.keyShare(1, 2, 3); + const backup = MPC.keyShare(2, 2, 3); + const bitgo = MPC.keyShare(3, 2, 3); + + expectedUserPShare = MPC.keyCombine(user.uShare, [backup.yShares[1], bitgo.yShares[1]]).pShare; + expectedBackupPShare = MPC.keyCombine(backup.uShare, [user.yShares[2], bitgo.yShares[2]]).pShare; + + userSigningMaterial = { + uShare: user.uShare, + bitgoYShare: bitgo.yShares[1], + backupYShare: backup.yShares[1], + }; + backupSigningMaterial = { + uShare: backup.uShare, + bitgoYShare: bitgo.yShares[2], + userYShare: user.yShares[2], + }; }); - let utils: EddsaMPCv2Utils; - beforeEach(() => { const mockBitGo = {} as unknown as BitGoBase; const mockCoin = {} as unknown as IBaseCoin; utils = new EddsaMPCv2Utils(mockBitGo, mockCoin); }); - function deriveScalar(seedHex: string): string { - const { createHash } = require('crypto'); - const seedBytes = Buffer.from(seedHex, 'hex'); - const hash = createHash('sha512').update(seedBytes).digest(); - const scalar = Buffer.from(hash.subarray(0, 32)); - scalar[0] &= 248; - scalar[31] &= 127; - scalar[31] |= 64; - return scalar.toString('hex'); - } - - it('returns EddsaRetrofitData for user and backup with matching expectedPk', () => { - const result = utils.getMpcV2RetrofitDataFromMpcV1Keys({ - mpcv1UserKeyShare: userMpcV1Key, - mpcv1BackupKeyShare: backupMpcV1Key, + it('derives matching expectedPk and chainCode for user and backup from real MPCv1 key combine', async () => { + const { userRetrofitData, backupRetrofitData } = await utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: JSON.stringify(userSigningMaterial), + mpcv1BackupKeyShare: JSON.stringify(backupSigningMaterial), }); - const { userRetrofitData, backupRetrofitData } = result as { - userRetrofitData: EddsaRetrofitData; - backupRetrofitData: EddsaRetrofitData; - }; + assert.strictEqual(userRetrofitData.expectedPk, expectedUserPShare.y); + assert.strictEqual(backupRetrofitData.expectedPk, expectedBackupPShare.y); + assert.strictEqual(userRetrofitData.expectedPk, backupRetrofitData.expectedPk); - assert.strictEqual(userRetrofitData.expectedPk, aggregatePk); - assert.strictEqual(backupRetrofitData.expectedPk, aggregatePk); + assert.strictEqual(userRetrofitData.chainCode, expectedUserPShare.chaincode); + assert.strictEqual(backupRetrofitData.chainCode, expectedBackupPShare.chaincode); + assert.strictEqual(userRetrofitData.chainCode, backupRetrofitData.chainCode); }); - it('returns correct chainCode per party', () => { - const { userRetrofitData, backupRetrofitData } = utils.getMpcV2RetrofitDataFromMpcV1Keys({ - mpcv1UserKeyShare: userMpcV1Key, - mpcv1BackupKeyShare: backupMpcV1Key, + it('derives s_i_0 as the combined pShare.u (clamped scalar) for each party', async () => { + const { userRetrofitData, backupRetrofitData } = await utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: JSON.stringify(userSigningMaterial), + mpcv1BackupKeyShare: JSON.stringify(backupSigningMaterial), }); - assert.strictEqual(userRetrofitData.chainCode, userChaincode); - assert.strictEqual(backupRetrofitData.chainCode, backupChaincode); + assert.strictEqual(userRetrofitData.s_i_0, expectedUserPShare.u); + assert.strictEqual(backupRetrofitData.s_i_0, expectedBackupPShare.u); + assert.notStrictEqual(userRetrofitData.s_i_0, backupRetrofitData.s_i_0); }); - it('returns correctly clamped s_i_0 scalars', () => { - const { userRetrofitData, backupRetrofitData } = utils.getMpcV2RetrofitDataFromMpcV1Keys({ - mpcv1UserKeyShare: userMpcV1Key, - mpcv1BackupKeyShare: backupMpcV1Key, + it('throws if user key is missing uShare', async () => { + const keyNoUShare = JSON.stringify({ + bitgoYShare: userSigningMaterial.bitgoYShare, + backupYShare: userSigningMaterial.backupYShare, }); - - assert.strictEqual(userRetrofitData.s_i_0, deriveScalar(userSeed)); - assert.strictEqual(backupRetrofitData.s_i_0, deriveScalar(backupSeed)); + await assert.rejects( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: keyNoUShare, + mpcv1BackupKeyShare: JSON.stringify(backupSigningMaterial), + }), + /MPCv1 key material missing uShare/ + ); }); - it('scalar byte[0] has low 3 bits cleared, byte[31] has bit7 cleared and bit6 set', () => { - const { userRetrofitData } = utils.getMpcV2RetrofitDataFromMpcV1Keys({ - mpcv1UserKeyShare: userMpcV1Key, - mpcv1BackupKeyShare: backupMpcV1Key, + it('throws if user key is missing bitgoYShare', async () => { + const keyNoBitgoYShare = JSON.stringify({ + uShare: userSigningMaterial.uShare, + backupYShare: userSigningMaterial.backupYShare, }); + await assert.rejects( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: keyNoBitgoYShare, + mpcv1BackupKeyShare: JSON.stringify(backupSigningMaterial), + }), + /MPCv1 key material missing bitgoYShare/ + ); + }); - const scalarBytes = Buffer.from(userRetrofitData.s_i_0, 'hex'); - assert.strictEqual(scalarBytes[0] & 0b111, 0, 'byte[0] low 3 bits should be cleared'); - assert.strictEqual(scalarBytes[31] & 0b10000000, 0, 'byte[31] bit7 should be cleared'); - assert.strictEqual(scalarBytes[31] & 0b01000000, 0b01000000, 'byte[31] bit6 should be set'); + it('throws if user key is missing backupYShare', async () => { + const keyNoBackupYShare = JSON.stringify({ + uShare: userSigningMaterial.uShare, + bitgoYShare: userSigningMaterial.bitgoYShare, + }); + await assert.rejects( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: keyNoBackupYShare, + mpcv1BackupKeyShare: JSON.stringify(backupSigningMaterial), + }), + /User MPCv1 key material missing backupYShare/ + ); }); - it('throws if user key is missing pShare.y', () => { - const keyNoPShare = JSON.stringify({ - uShare: { i: 1, seed: userSeed, chaincode: userChaincode }, - bitgoYShare: { u: 'x' }, + it('throws if backup key is missing uShare', async () => { + const keyNoUShare = JSON.stringify({ + bitgoYShare: backupSigningMaterial.bitgoYShare, + userYShare: backupSigningMaterial.userYShare, }); - assert.throws( + await assert.rejects( () => utils.getMpcV2RetrofitDataFromMpcV1Keys({ - mpcv1UserKeyShare: keyNoPShare, - mpcv1BackupKeyShare: backupMpcV1Key, + mpcv1UserKeyShare: JSON.stringify(userSigningMaterial), + mpcv1BackupKeyShare: keyNoUShare, }), - /MPCv1 user key missing pShare\.y/ + /MPCv1 key material missing uShare/ ); }); - it('throws if user key is missing uShare.seed', () => { - const keyNoSeed = JSON.stringify({ - uShare: { i: 1, chaincode: userChaincode }, - pShare: { y: aggregatePk }, - bitgoYShare: { u: 'x' }, + it('throws if backup key is missing bitgoYShare', async () => { + const keyNoBitgoYShare = JSON.stringify({ + uShare: backupSigningMaterial.uShare, + userYShare: backupSigningMaterial.userYShare, }); - assert.throws( + await assert.rejects( () => utils.getMpcV2RetrofitDataFromMpcV1Keys({ - mpcv1UserKeyShare: keyNoSeed, - mpcv1BackupKeyShare: backupMpcV1Key, + mpcv1UserKeyShare: JSON.stringify(userSigningMaterial), + mpcv1BackupKeyShare: keyNoBitgoYShare, }), - /MPCv1 key missing uShare\.seed/ + /MPCv1 key material missing bitgoYShare/ ); }); - it('throws if backup key is missing uShare.seed', () => { - const backupNoSeed = JSON.stringify({ - uShare: { i: 2, chaincode: backupChaincode }, - bitgoYShare: { u: 'x' }, + it('throws if backup key is missing userYShare', async () => { + const keyNoUserYShare = JSON.stringify({ + uShare: backupSigningMaterial.uShare, + bitgoYShare: backupSigningMaterial.bitgoYShare, }); - assert.throws( + await assert.rejects( () => utils.getMpcV2RetrofitDataFromMpcV1Keys({ - mpcv1UserKeyShare: userMpcV1Key, - mpcv1BackupKeyShare: backupNoSeed, + mpcv1UserKeyShare: JSON.stringify(userSigningMaterial), + mpcv1BackupKeyShare: keyNoUserYShare, }), - /MPCv1 key missing uShare\.seed/ + /Backup MPCv1 key material missing userYShare/ ); }); }); 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 02febddce3..aba2126ef2 100644 --- a/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts +++ b/modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts @@ -2,6 +2,20 @@ import { decode } from 'cbor-x'; import { isLeft } from 'fp-ts/Either'; import * as t from 'io-ts'; +/** + * Retrofit data derived from an existing MPCv1 EdDSA key share, used to seed + * an MPCv2 (MPS) DKG retrofit ceremony (`ed25519_dkg_round0_import`). + * + * @property s_i_0 - Party's clamped additive scalar (pShare.u), 32 bytes LE hex. + * @property expectedPk - Aggregate Ed25519 public key (pShare.y), 32 bytes hex. + * @property chainCode - Combined 32-byte BIP32 chain code (pShare.chaincode), hex. + */ +export type EddsaRetrofitData = { + s_i_0: string; + expectedPk: string; + chainCode: string; +}; + export const ReducedKeyShareType = t.type({ keyShare: t.array(t.number), pub: t.array(t.number),