Skip to content

Commit 7cf80ae

Browse files
Marzooqabitgobot
authored andcommitted
feat(sdk-core): getUserAndBackupSession + createKeychains retrofit wiring
Wire the retrofit path into EddsaMPCv2Utils.createKeychains(). When a retrofit payload is supplied, getUserAndBackupSession() initialises user and backup DKG sessions with EddsaRetrofitData (via getMpcV2RetrofitDataFromMpcV1Keys) instead of fresh randomness. The R1 request body includes walletId so the server-side isRound1RetrofitDKG() detection kicks in. Changes: - Add retrofit?: DecryptedRetrofitPayload to createKeychains() params - Add private async getUserAndBackupSession() that branches on retrofit - Replace inline DKG construction with getUserAndBackupSession() call - Extend sendKeyGenerationRound1/BySender payload type to allow walletId - Spread walletId into R1 payload when retrofit.walletId is present - Tests for getUserAndBackupSession (no-retrofit and retrofit paths) and for walletId presence/absence in the captured R1 payload Follows the same pattern as ecdsaMPCv2.ts getUserAndBackupSession (line 639) and the walletId spread (line 126-129). Ticket: WCI-1264 Session-Id: 597157b8-fee3-4515-b21b-4030e08362e8 Task-Id: 15dfe9c9-d429-4559-b0fc-771ada5e3c3a
1 parent 0ba36a2 commit 7cf80ae

3 files changed

Lines changed: 172 additions & 16 deletions

File tree

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { EddsaMPCv2KeyGenCallbacks } from '../../../wallet/iWallets';
1616
import { ed25519 } from '@noble/curves/ed25519';
1717
import { EddsaMPSDkg, EddsaMPSDsg, MPSComms, MPSTypes, MPSUtil } from '@bitgo/sdk-lib-mpc';
1818
import { KeychainsTriplet } from '../../../baseCoin';
19-
import { AddKeychainOptions, Keychain, KeyType, WebauthnKeyEncryptionInfo } from '../../../keychain';
19+
import { AddKeychainOptions, DecryptedRetrofitPayload, Keychain, KeyType, WebauthnKeyEncryptionInfo } from '../../../keychain';
2020
import { envRequiresBitgoPubGpgKeyConfig, isBitgoEddsaMpcv2PubKey } from '../../../tss/bitgoPubKeys';
2121
import { getBitgoSignatureShare, getTxRequest, sendSignatureShareV2, sendTxRequest } from '../../../tss/common';
2222
import { decodeWithCodec } from '../../codecs';
@@ -63,6 +63,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
6363
passphrase: string;
6464
enterprise: string;
6565
originalPasscodeEncryptionCode?: string;
66+
retrofit?: DecryptedRetrofitPayload;
6667
webauthnInfo?: WebauthnKeyEncryptionInfo;
6768
encryptionVersion?: EncryptionVersion;
6869
// Wallet Safes v1 (@experimental): tags the resulting user/backup/bitgo root keys with this safe.
@@ -93,8 +94,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
9394
const bitgoPk = await MPSComms.extractEd25519PublicKey(bitgoKeyObj);
9495

9596
// Create DKG sessions for user (party 0) and backup (party 1)
96-
const userDkg = new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.USER);
97-
const backupDkg = new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.BACKUP);
97+
const { userDkg, backupDkg } = await this.getUserAndBackupSession(params.retrofit);
9898

9999
// #region round 1
100100
await userDkg.initDkg(userSk, [backupPk, bitgoPk]);
@@ -116,6 +116,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
116116
backupGpgPublicKey,
117117
userMsg1: userSignedMsg1,
118118
backupMsg1: backupSignedMsg1,
119+
...(params.retrofit?.walletId ? { walletId: params.retrofit.walletId } : {}),
119120
},
120121
params.safeId
121122
);
@@ -459,15 +460,15 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
459460

460461
async sendKeyGenerationRound1(
461462
enterprise: string,
462-
payload: EddsaMPCv2KeyGenRound1Request,
463+
payload: EddsaMPCv2KeyGenRound1Request & { walletId?: string },
463464
safeId?: string
464465
): Promise<EddsaMPCv2KeyGenRound1Response> {
465466
return this.sendKeyGenerationRound1BySender(KeyGenSenderForEnterprise(this.bitgo, enterprise, safeId), payload);
466467
}
467468

468469
async sendKeyGenerationRound1BySender(
469470
senderFn: EddsaMPCv2KeyGenSendFn<EddsaMPCv2KeyGenRound1Response>,
470-
payload: EddsaMPCv2KeyGenRound1Request
471+
payload: EddsaMPCv2KeyGenRound1Request & { walletId?: string }
471472
): Promise<EddsaMPCv2KeyGenRound1Response> {
472473
return senderFn(MPCv2KeyGenStateEnum['MPCv2-R1'], payload);
473474
}
@@ -1070,6 +1071,26 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
10701071

10711072
// #region retrofit
10721073

1074+
private async getUserAndBackupSession(retrofit?: DecryptedRetrofitPayload): Promise<{
1075+
userDkg: EddsaMPSDkg.DKG;
1076+
backupDkg: EddsaMPSDkg.DKG;
1077+
}> {
1078+
if (retrofit) {
1079+
const { userRetrofitData, backupRetrofitData } = await this.getMpcV2RetrofitDataFromMpcV1Keys({
1080+
mpcv1UserKeyShare: retrofit.decryptedUserKey,
1081+
mpcv1BackupKeyShare: retrofit.decryptedBackupKey,
1082+
});
1083+
return {
1084+
userDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.USER, userRetrofitData),
1085+
backupDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.BACKUP, backupRetrofitData),
1086+
};
1087+
}
1088+
return {
1089+
userDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.USER),
1090+
backupDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.BACKUP),
1091+
};
1092+
}
1093+
10731094
async getMpcV2RetrofitDataFromMpcV1Keys(params: { mpcv1UserKeyShare: string; mpcv1BackupKeyShare: string }): Promise<{
10741095
userRetrofitData: MPSTypes.EddsaRetrofitData;
10751096
backupRetrofitData: MPSTypes.EddsaRetrofitData;

modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2467,3 +2467,149 @@ describe('EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Keys', () => {
24672467
);
24682468
});
24692469
});
2470+
2471+
describe('EddsaMPCv2Utils.getUserAndBackupSession', () => {
2472+
let utils: EddsaMPCv2Utils;
2473+
let userSigningMaterial: Record<string, unknown>;
2474+
let backupSigningMaterial: Record<string, unknown>;
2475+
2476+
before(async () => {
2477+
const MPC = await getInitializedMpcInstance();
2478+
const user = MPC.keyShare(1, 2, 3);
2479+
const backup = MPC.keyShare(2, 2, 3);
2480+
const bitgo = MPC.keyShare(3, 2, 3);
2481+
userSigningMaterial = {
2482+
uShare: user.uShare,
2483+
bitgoYShare: bitgo.yShares[1],
2484+
backupYShare: backup.yShares[1],
2485+
};
2486+
backupSigningMaterial = {
2487+
uShare: backup.uShare,
2488+
bitgoYShare: bitgo.yShares[2],
2489+
userYShare: user.yShares[2],
2490+
};
2491+
});
2492+
2493+
beforeEach(() => {
2494+
const mockBitGo = {} as unknown as BitGoBase;
2495+
const mockCoin = {} as unknown as IBaseCoin;
2496+
utils = new EddsaMPCv2Utils(mockBitGo, mockCoin);
2497+
});
2498+
2499+
afterEach(() => {
2500+
sinon.restore();
2501+
});
2502+
2503+
it('returns plain DKG sessions when retrofit is undefined', async () => {
2504+
const { userDkg, backupDkg } = await (utils as any).getUserAndBackupSession(undefined);
2505+
assert.ok(userDkg, 'user DKG should be created');
2506+
assert.ok(backupDkg, 'backup DKG should be created');
2507+
});
2508+
2509+
it('returns retrofit-seeded DKG sessions when retrofit payload is supplied', async () => {
2510+
const retrofit = {
2511+
decryptedUserKey: JSON.stringify(userSigningMaterial),
2512+
decryptedBackupKey: JSON.stringify(backupSigningMaterial),
2513+
walletId: 'wallet-123',
2514+
};
2515+
const { userDkg, backupDkg } = await (utils as any).getUserAndBackupSession(retrofit);
2516+
assert.ok(userDkg, 'user DKG should be created with retrofit data');
2517+
assert.ok(backupDkg, 'backup DKG should be created with retrofit data');
2518+
});
2519+
});
2520+
2521+
describe('EddsaMPCv2Utils.createKeychains with retrofit wiring', () => {
2522+
let utils: EddsaMPCv2Utils;
2523+
let userSigningMaterial: Record<string, unknown>;
2524+
let backupSigningMaterial: Record<string, unknown>;
2525+
let bitgoGpgPublicKeyArmored: string;
2526+
const enterprise = 'enterprise-id';
2527+
const sessionId = 'session-001';
2528+
const walletId = 'wallet-retrofit-123';
2529+
2530+
before(async () => {
2531+
const MPC = await getInitializedMpcInstance();
2532+
const user = MPC.keyShare(1, 2, 3);
2533+
const backup = MPC.keyShare(2, 2, 3);
2534+
const bitgo = MPC.keyShare(3, 2, 3);
2535+
userSigningMaterial = {
2536+
uShare: user.uShare,
2537+
bitgoYShare: bitgo.yShares[1],
2538+
backupYShare: backup.yShares[1],
2539+
};
2540+
backupSigningMaterial = {
2541+
uShare: backup.uShare,
2542+
bitgoYShare: bitgo.yShares[2],
2543+
userYShare: user.yShares[2],
2544+
};
2545+
// Generate a real Ed25519 GPG key to stand in for the BitGo GPG key
2546+
const bitgoGpgKeyPair = await generateGPGKeyPair('ed25519');
2547+
bitgoGpgPublicKeyArmored = bitgoGpgKeyPair.publicKey;
2548+
});
2549+
2550+
beforeEach(() => {
2551+
const mockBitGo = {
2552+
getEnv: sinon.stub().returns('dev'),
2553+
encrypt: sinon.stub().resolves('encrypted'),
2554+
} as any;
2555+
const mockKeychains = {
2556+
add: sinon
2557+
.stub()
2558+
.callsFake((params: any) =>
2559+
Promise.resolve({ id: `${params.source}-key-id`, commonKeychain: 'a'.repeat(128), isMPCv2: true })
2560+
),
2561+
};
2562+
const mockCoin = {
2563+
keychains: sinon.stub().returns(mockKeychains),
2564+
} as any;
2565+
2566+
utils = new EddsaMPCv2Utils(mockBitGo, mockCoin);
2567+
sinon.stub(utils, 'getBitgoGpgPubkeyBasedOnFeatureFlags' as any).resolves({ eddsaMpcv2PublicKey: null });
2568+
// Use a real armored GPG public key so pgp.readKey() succeeds inside createKeychains
2569+
(utils as any).bitgoEddsaMpcv2PublicGpgKey = { armor: () => bitgoGpgPublicKeyArmored };
2570+
sinon.stub(utils as any, 'addBitgoKeychain').resolves({ id: 'bitgo-key-id', commonKeychain: 'a'.repeat(128) });
2571+
});
2572+
2573+
afterEach(() => {
2574+
sinon.restore();
2575+
});
2576+
2577+
it('spreads walletId into round-1 payload when retrofit is provided', async () => {
2578+
const capturedPayloads: any[] = [];
2579+
sinon.stub(utils, 'sendKeyGenerationRound1').callsFake(async (_enterprise: string, payload: any) => {
2580+
capturedPayloads.push(payload);
2581+
// Return a bad bitgoMsg1 to short-circuit the ceremony after R1 capture
2582+
return { sessionId: sessionId as any, bitgoMsg1: { message: '', signature: '' } as any };
2583+
});
2584+
2585+
const retrofit = {
2586+
decryptedUserKey: JSON.stringify(userSigningMaterial),
2587+
decryptedBackupKey: JSON.stringify(backupSigningMaterial),
2588+
walletId,
2589+
};
2590+
2591+
await assert.rejects(
2592+
() => utils.createKeychains({ passphrase: 'test', enterprise, retrofit }),
2593+
() => true
2594+
);
2595+
2596+
assert.strictEqual(capturedPayloads.length, 1, 'sendKeyGenerationRound1 should be called once');
2597+
assert.strictEqual(capturedPayloads[0].walletId, walletId, 'walletId must be present in round-1 payload');
2598+
});
2599+
2600+
it('omits walletId from round-1 payload when retrofit is absent', async () => {
2601+
const capturedPayloads: any[] = [];
2602+
sinon.stub(utils, 'sendKeyGenerationRound1').callsFake(async (_enterprise: string, payload: any) => {
2603+
capturedPayloads.push(payload);
2604+
return { sessionId: sessionId as any, bitgoMsg1: { message: '', signature: '' } as any };
2605+
});
2606+
2607+
await assert.rejects(
2608+
() => utils.createKeychains({ passphrase: 'test', enterprise }),
2609+
() => true
2610+
);
2611+
2612+
assert.strictEqual(capturedPayloads.length, 1, 'sendKeyGenerationRound1 should be called once');
2613+
assert.strictEqual(capturedPayloads[0].walletId, undefined, 'walletId must be absent when no retrofit');
2614+
});
2615+
});

modules/sdk-lib-mpc/test/unit/tss/eddsa/dkg.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -354,11 +354,7 @@ describe('EdDSA MPS DKG', function () {
354354
// Eddsa.keyCombine stores pShare.chaincode as bigIntToBufferBE — match that encoding
355355
const chainCode = bigIntToBufferBE(aggChaincode, 32).toString('hex');
356356

357-
<<<<<<< HEAD
358357
return parties.map((party) => ({
359-
=======
360-
return parties.map((party, idx) => ({
361-
>>>>>>> 6117b6d19a (feat(sdk-lib-mpc): eddsaRetrofitData type + DKG retrofit constructor + getFirstMessage routing)
362358
s_i_0: bigIntToBufferLE(party.u, 32).toString('hex'),
363359
expectedPk,
364360
chainCode,
@@ -393,19 +389,12 @@ describe('EdDSA MPS DKG', function () {
393389
[retrofitUser, retrofitBackup, retrofitBitgo] = buildRetrofitData(seeds);
394390
});
395391

396-
<<<<<<< HEAD
397-
it('each party has a distinct s_i_0 but shared expectedPk', function () {
398-
=======
399392
it('each party has a distinct s_i_0 but shared expectedPk and chainCode', function () {
400-
>>>>>>> 6117b6d19a (feat(sdk-lib-mpc): eddsaRetrofitData type + DKG retrofit constructor + getFirstMessage routing)
401393
assert.notStrictEqual(retrofitUser.s_i_0, retrofitBackup.s_i_0, 'user and backup s_i_0 must differ');
402394
assert.notStrictEqual(retrofitBackup.s_i_0, retrofitBitgo.s_i_0, 'backup and bitgo s_i_0 must differ');
403395
assert.strictEqual(retrofitUser.expectedPk, retrofitBackup.expectedPk, 'all parties share expectedPk');
404396
assert.strictEqual(retrofitBackup.expectedPk, retrofitBitgo.expectedPk, 'all parties share expectedPk');
405-
<<<<<<< HEAD
406-
=======
407397
assert.strictEqual(retrofitUser.chainCode, retrofitBackup.chainCode, 'all parties share chainCode');
408-
>>>>>>> 6117b6d19a (feat(sdk-lib-mpc): eddsaRetrofitData type + DKG retrofit constructor + getFirstMessage routing)
409398
});
410399

411400
it('should route getFirstMessage through ed25519_dkg_round0_import and all parties agree on public key', async function () {

0 commit comments

Comments
 (0)