Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<ReturnType<typeof getInitializedMpcInstance>>
): 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
}

/**
Expand Down
152 changes: 152 additions & 0 deletions modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
let backupSigningMaterial: Record<string, unknown>;
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/
);
});
});
14 changes: 14 additions & 0 deletions modules/sdk-lib-mpc/src/tss/eddsa-mps/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
Loading