Skip to content

Commit 800cab5

Browse files
committed
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 045559a commit 800cab5

2 files changed

Lines changed: 172 additions & 5 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
@@ -2330,3 +2330,149 @@ describe('EddsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Keys', () => {
23302330
);
23312331
});
23322332
});
2333+
2334+
describe('EddsaMPCv2Utils.getUserAndBackupSession', () => {
2335+
let utils: EddsaMPCv2Utils;
2336+
let userSigningMaterial: Record<string, unknown>;
2337+
let backupSigningMaterial: Record<string, unknown>;
2338+
2339+
before(async () => {
2340+
const MPC = await getInitializedMpcInstance();
2341+
const user = MPC.keyShare(1, 2, 3);
2342+
const backup = MPC.keyShare(2, 2, 3);
2343+
const bitgo = MPC.keyShare(3, 2, 3);
2344+
userSigningMaterial = {
2345+
uShare: user.uShare,
2346+
bitgoYShare: bitgo.yShares[1],
2347+
backupYShare: backup.yShares[1],
2348+
};
2349+
backupSigningMaterial = {
2350+
uShare: backup.uShare,
2351+
bitgoYShare: bitgo.yShares[2],
2352+
userYShare: user.yShares[2],
2353+
};
2354+
});
2355+
2356+
beforeEach(() => {
2357+
const mockBitGo = {} as unknown as BitGoBase;
2358+
const mockCoin = {} as unknown as IBaseCoin;
2359+
utils = new EddsaMPCv2Utils(mockBitGo, mockCoin);
2360+
});
2361+
2362+
afterEach(() => {
2363+
sinon.restore();
2364+
});
2365+
2366+
it('returns plain DKG sessions when retrofit is undefined', async () => {
2367+
const { userDkg, backupDkg } = await (utils as any).getUserAndBackupSession(undefined);
2368+
assert.ok(userDkg, 'user DKG should be created');
2369+
assert.ok(backupDkg, 'backup DKG should be created');
2370+
});
2371+
2372+
it('returns retrofit-seeded DKG sessions when retrofit payload is supplied', async () => {
2373+
const retrofit = {
2374+
decryptedUserKey: JSON.stringify(userSigningMaterial),
2375+
decryptedBackupKey: JSON.stringify(backupSigningMaterial),
2376+
walletId: 'wallet-123',
2377+
};
2378+
const { userDkg, backupDkg } = await (utils as any).getUserAndBackupSession(retrofit);
2379+
assert.ok(userDkg, 'user DKG should be created with retrofit data');
2380+
assert.ok(backupDkg, 'backup DKG should be created with retrofit data');
2381+
});
2382+
});
2383+
2384+
describe('EddsaMPCv2Utils.createKeychains with retrofit wiring', () => {
2385+
let utils: EddsaMPCv2Utils;
2386+
let userSigningMaterial: Record<string, unknown>;
2387+
let backupSigningMaterial: Record<string, unknown>;
2388+
let bitgoGpgPublicKeyArmored: string;
2389+
const enterprise = 'enterprise-id';
2390+
const sessionId = 'session-001';
2391+
const walletId = 'wallet-retrofit-123';
2392+
2393+
before(async () => {
2394+
const MPC = await getInitializedMpcInstance();
2395+
const user = MPC.keyShare(1, 2, 3);
2396+
const backup = MPC.keyShare(2, 2, 3);
2397+
const bitgo = MPC.keyShare(3, 2, 3);
2398+
userSigningMaterial = {
2399+
uShare: user.uShare,
2400+
bitgoYShare: bitgo.yShares[1],
2401+
backupYShare: backup.yShares[1],
2402+
};
2403+
backupSigningMaterial = {
2404+
uShare: backup.uShare,
2405+
bitgoYShare: bitgo.yShares[2],
2406+
userYShare: user.yShares[2],
2407+
};
2408+
// Generate a real Ed25519 GPG key to stand in for the BitGo GPG key
2409+
const bitgoGpgKeyPair = await generateGPGKeyPair('ed25519');
2410+
bitgoGpgPublicKeyArmored = bitgoGpgKeyPair.publicKey;
2411+
});
2412+
2413+
beforeEach(() => {
2414+
const mockBitGo = {
2415+
getEnv: sinon.stub().returns('dev'),
2416+
encrypt: sinon.stub().resolves('encrypted'),
2417+
} as any;
2418+
const mockKeychains = {
2419+
add: sinon
2420+
.stub()
2421+
.callsFake((params: any) =>
2422+
Promise.resolve({ id: `${params.source}-key-id`, commonKeychain: 'a'.repeat(128), isMPCv2: true })
2423+
),
2424+
};
2425+
const mockCoin = {
2426+
keychains: sinon.stub().returns(mockKeychains),
2427+
} as any;
2428+
2429+
utils = new EddsaMPCv2Utils(mockBitGo, mockCoin);
2430+
sinon.stub(utils, 'getBitgoGpgPubkeyBasedOnFeatureFlags' as any).resolves({ eddsaMpcv2PublicKey: null });
2431+
// Use a real armored GPG public key so pgp.readKey() succeeds inside createKeychains
2432+
(utils as any).bitgoEddsaMpcv2PublicGpgKey = { armor: () => bitgoGpgPublicKeyArmored };
2433+
sinon.stub(utils as any, 'addBitgoKeychain').resolves({ id: 'bitgo-key-id', commonKeychain: 'a'.repeat(128) });
2434+
});
2435+
2436+
afterEach(() => {
2437+
sinon.restore();
2438+
});
2439+
2440+
it('spreads walletId into round-1 payload when retrofit is provided', async () => {
2441+
const capturedPayloads: any[] = [];
2442+
sinon.stub(utils, 'sendKeyGenerationRound1').callsFake(async (_enterprise: string, payload: any) => {
2443+
capturedPayloads.push(payload);
2444+
// Return a bad bitgoMsg1 to short-circuit the ceremony after R1 capture
2445+
return { sessionId: sessionId as any, bitgoMsg1: { message: '', signature: '' } as any };
2446+
});
2447+
2448+
const retrofit = {
2449+
decryptedUserKey: JSON.stringify(userSigningMaterial),
2450+
decryptedBackupKey: JSON.stringify(backupSigningMaterial),
2451+
walletId,
2452+
};
2453+
2454+
await assert.rejects(
2455+
() => utils.createKeychains({ passphrase: 'test', enterprise, retrofit }),
2456+
() => true
2457+
);
2458+
2459+
assert.strictEqual(capturedPayloads.length, 1, 'sendKeyGenerationRound1 should be called once');
2460+
assert.strictEqual(capturedPayloads[0].walletId, walletId, 'walletId must be present in round-1 payload');
2461+
});
2462+
2463+
it('omits walletId from round-1 payload when retrofit is absent', async () => {
2464+
const capturedPayloads: any[] = [];
2465+
sinon.stub(utils, 'sendKeyGenerationRound1').callsFake(async (_enterprise: string, payload: any) => {
2466+
capturedPayloads.push(payload);
2467+
return { sessionId: sessionId as any, bitgoMsg1: { message: '', signature: '' } as any };
2468+
});
2469+
2470+
await assert.rejects(
2471+
() => utils.createKeychains({ passphrase: 'test', enterprise }),
2472+
() => true
2473+
);
2474+
2475+
assert.strictEqual(capturedPayloads.length, 1, 'sendKeyGenerationRound1 should be called once');
2476+
assert.strictEqual(capturedPayloads[0].walletId, undefined, 'walletId must be absent when no retrofit');
2477+
});
2478+
});

0 commit comments

Comments
 (0)