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..64b4b5df9c 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -50,6 +50,7 @@ import { BaseEddsaUtils } from './base'; import { resolveEffectiveTxParams } from '../recipientUtils'; import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './eddsaMPCv2KeyGenSender'; import { EddsaMPCv2RecoveryKeyShares } from './types'; +import { SigningMaterial } from '../../../tss'; export class EddsaMPCv2Utils extends BaseEddsaUtils { private static readonly MPS_DSG_SIGNING_USER_GPG_KEY = 'MPS_DSG_SIGNING_USER_GPG_KEY'; @@ -1066,6 +1067,74 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { return sendTxRequest(this.bitgo, txRequestResolved.walletId, txRequestResolved.txRequestId, requestType, reqId); } // #endregion + + // #region retrofit + + 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 + ); + + 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( + 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: pShare.u, + expectedPk: pShare.y, + chainCode: pShare.chaincode, + }; + } + + // #endregion } /** 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..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 @@ -2178,3 +2178,155 @@ describe('EddsaMPCv2Utils.createKeychainsWithExternalSigner', function () { ); }); }); + +describe('EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Keys', () => { + 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 }; + + 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], + }; + }); + + beforeEach(() => { + const mockBitGo = {} as unknown as BitGoBase; + const mockCoin = {} as unknown as IBaseCoin; + utils = new EddsaMPCv2Utils(mockBitGo, mockCoin); + }); + + 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), + }); + + assert.strictEqual(userRetrofitData.expectedPk, expectedUserPShare.y); + assert.strictEqual(backupRetrofitData.expectedPk, expectedBackupPShare.y); + assert.strictEqual(userRetrofitData.expectedPk, backupRetrofitData.expectedPk); + + assert.strictEqual(userRetrofitData.chainCode, expectedUserPShare.chaincode); + assert.strictEqual(backupRetrofitData.chainCode, expectedBackupPShare.chaincode); + assert.strictEqual(userRetrofitData.chainCode, backupRetrofitData.chainCode); + }); + + 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.s_i_0, expectedUserPShare.u); + assert.strictEqual(backupRetrofitData.s_i_0, expectedBackupPShare.u); + assert.notStrictEqual(userRetrofitData.s_i_0, backupRetrofitData.s_i_0); + }); + + it('throws if user key is missing uShare', async () => { + const keyNoUShare = JSON.stringify({ + bitgoYShare: userSigningMaterial.bitgoYShare, + backupYShare: userSigningMaterial.backupYShare, + }); + await assert.rejects( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: keyNoUShare, + mpcv1BackupKeyShare: JSON.stringify(backupSigningMaterial), + }), + /MPCv1 key material missing uShare/ + ); + }); + + 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/ + ); + }); + + 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 backup key is missing uShare', async () => { + const keyNoUShare = JSON.stringify({ + bitgoYShare: backupSigningMaterial.bitgoYShare, + userYShare: backupSigningMaterial.userYShare, + }); + await assert.rejects( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: JSON.stringify(userSigningMaterial), + mpcv1BackupKeyShare: keyNoUShare, + }), + /MPCv1 key material missing uShare/ + ); + }); + + it('throws if backup key is missing bitgoYShare', async () => { + const keyNoBitgoYShare = JSON.stringify({ + uShare: backupSigningMaterial.uShare, + userYShare: backupSigningMaterial.userYShare, + }); + await assert.rejects( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: JSON.stringify(userSigningMaterial), + mpcv1BackupKeyShare: keyNoBitgoYShare, + }), + /MPCv1 key material missing bitgoYShare/ + ); + }); + + it('throws if backup key is missing userYShare', async () => { + const keyNoUserYShare = JSON.stringify({ + uShare: backupSigningMaterial.uShare, + bitgoYShare: backupSigningMaterial.bitgoYShare, + }); + await assert.rejects( + () => + utils.getMpcV2RetrofitDataFromMpcV1Keys({ + mpcv1UserKeyShare: JSON.stringify(userSigningMaterial), + mpcv1BackupKeyShare: keyNoUserYShare, + }), + /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),