Skip to content

Commit a68583c

Browse files
authored
feat(sdk-core): getUserAndBackupSession + createKeychains retrofit wiring
2 parents e3cd214 + ea83766 commit a68583c

2 files changed

Lines changed: 178 additions & 5 deletions

File tree

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

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ 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 {
20+
AddKeychainOptions,
21+
DecryptedRetrofitPayload,
22+
Keychain,
23+
KeyType,
24+
WebauthnKeyEncryptionInfo,
25+
} from '../../../keychain';
2026
import { envRequiresBitgoPubGpgKeyConfig, isBitgoEddsaMpcv2PubKey } from '../../../tss/bitgoPubKeys';
2127
import { getBitgoSignatureShare, getTxRequest, sendSignatureShareV2, sendTxRequest } from '../../../tss/common';
2228
import { decodeWithCodec } from '../../codecs';
@@ -63,6 +69,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
6369
passphrase: string;
6470
enterprise: string;
6571
originalPasscodeEncryptionCode?: string;
72+
retrofit?: DecryptedRetrofitPayload;
6673
webauthnInfo?: WebauthnKeyEncryptionInfo;
6774
encryptionVersion?: EncryptionVersion;
6875
// Wallet Safes v1 (@experimental): tags the resulting user/backup/bitgo root keys with this safe.
@@ -93,8 +100,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
93100
const bitgoPk = await MPSComms.extractEd25519PublicKey(bitgoKeyObj);
94101

95102
// 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);
103+
const { userDkg, backupDkg } = await this.getUserAndBackupSession(params.retrofit);
98104

99105
// #region round 1
100106
await userDkg.initDkg(userSk, [backupPk, bitgoPk]);
@@ -116,6 +122,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
116122
backupGpgPublicKey,
117123
userMsg1: userSignedMsg1,
118124
backupMsg1: backupSignedMsg1,
125+
...(params.retrofit?.walletId ? { walletId: params.retrofit.walletId } : {}),
119126
},
120127
params.safeId
121128
);
@@ -459,15 +466,15 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
459466

460467
async sendKeyGenerationRound1(
461468
enterprise: string,
462-
payload: EddsaMPCv2KeyGenRound1Request,
469+
payload: EddsaMPCv2KeyGenRound1Request & { walletId?: string },
463470
safeId?: string
464471
): Promise<EddsaMPCv2KeyGenRound1Response> {
465472
return this.sendKeyGenerationRound1BySender(KeyGenSenderForEnterprise(this.bitgo, enterprise, safeId), payload);
466473
}
467474

468475
async sendKeyGenerationRound1BySender(
469476
senderFn: EddsaMPCv2KeyGenSendFn<EddsaMPCv2KeyGenRound1Response>,
470-
payload: EddsaMPCv2KeyGenRound1Request
477+
payload: EddsaMPCv2KeyGenRound1Request & { walletId?: string }
471478
): Promise<EddsaMPCv2KeyGenRound1Response> {
472479
return senderFn(MPCv2KeyGenStateEnum['MPCv2-R1'], payload);
473480
}
@@ -1070,6 +1077,26 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
10701077

10711078
// #region retrofit
10721079

1080+
private async getUserAndBackupSession(retrofit?: DecryptedRetrofitPayload): Promise<{
1081+
userDkg: EddsaMPSDkg.DKG;
1082+
backupDkg: EddsaMPSDkg.DKG;
1083+
}> {
1084+
if (retrofit) {
1085+
const { userRetrofitData, backupRetrofitData } = await this.getMpcV2RetrofitDataFromMpcV1Keys({
1086+
mpcv1UserKeyShare: retrofit.decryptedUserKey,
1087+
mpcv1BackupKeyShare: retrofit.decryptedBackupKey,
1088+
});
1089+
return {
1090+
userDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.USER, userRetrofitData),
1091+
backupDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.BACKUP, backupRetrofitData),
1092+
};
1093+
}
1094+
return {
1095+
userDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.USER),
1096+
backupDkg: new EddsaMPSDkg.DKG(3, 2, MPCv2PartiesEnum.BACKUP),
1097+
};
1098+
}
1099+
10731100
async getMpcV2RetrofitDataFromMpcV1Keys(params: { mpcv1UserKeyShare: string; mpcv1BackupKeyShare: string }): Promise<{
10741101
userRetrofitData: MPSTypes.EddsaRetrofitData;
10751102
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+
});

0 commit comments

Comments
 (0)