Skip to content

Commit 8f41533

Browse files
committed
fix: add EdDSA MPCv2 chain code support for address derivation
- Bump @bitgo/wasm-mps to 1.7.0 which exposes chaincode on the Share struct - Add shareChaincode field and getCommonKeychain() to DKG class in sdk-lib-mpc, returning pubkey+chaincode (128 hex chars) matching Eddsa.deriveUnhardened expectations - Use getCommonKeychain() in EddsaMPCv2Utils.createKeychains and pass full keychain to user/backup/bitgo keychains instead of just the 32-byte public key - Rename commonPublicKey to commonPublicKeychain in round 2 response handling to align with updated @bitgo/public-types and HSM response field name - Remove 64-char MPCv2 special case from getPublicKeyFromCommonKeychain; all EdDSA keychains are now uniformly 128 hex chars (pubkey + chaincode) - Update tests accordingly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> TICKET: WCI-230
1 parent 5727538 commit 8f41533

7 files changed

Lines changed: 53 additions & 43 deletions

File tree

modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,27 +1216,21 @@ describe('TSS Utils:', async function () {
12161216
}
12171217

12181218
describe('getPublicKeyFromCommonKeychain', function () {
1219-
// 32-byte ed25519 public key as hex (64 chars) — the format produced by DKG getSharePublicKey().toString('hex')
1220-
const mpcv2CommonKeychain = 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270';
1221-
// MPCv1 appends a 32-byte chaincode after the public key
1219+
// 32-byte ed25519 public key as hex (64 chars) + 32-byte chaincode (64 chars) = 128 chars
1220+
const pubHex = 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270';
12221221
const chaincode = '9d91c2e6353202cf61f8f275158b3468e9a00f7872fc2fd310b72cd026e2e2f9';
1223-
const mpcv1CommonKeychain = mpcv2CommonKeychain + chaincode;
1222+
const commonKeychain = pubHex + chaincode;
12241223

1225-
it('should decode to the same 32-byte public key for both MPCv1 (128 chars) and MPCv2 (64 chars)', function () {
1226-
mpcv1CommonKeychain.length.should.equal(128);
1227-
mpcv2CommonKeychain.length.should.equal(64);
1228-
1229-
const v1Result = TssUtils.getPublicKeyFromCommonKeychain(mpcv1CommonKeychain);
1230-
const v2Result = TssUtils.getPublicKeyFromCommonKeychain(mpcv2CommonKeychain);
1231-
1232-
v1Result.should.equal(v2Result);
1233-
v1Result.should.equal('ByMPeVxs7e8zGecu8n1M43Mq9qkxBSypNNwHeEu2N6vb');
1224+
it('should decode the 32-byte public key from a 128-char commonKeychain', function () {
1225+
commonKeychain.length.should.equal(128);
1226+
const result = TssUtils.getPublicKeyFromCommonKeychain(commonKeychain);
1227+
result.should.equal('ByMPeVxs7e8zGecu8n1M43Mq9qkxBSypNNwHeEu2N6vb');
12341228
});
12351229

12361230
it('should throw for an invalid commonKeychain length', function () {
12371231
should.throws(
12381232
() => TssUtils.getPublicKeyFromCommonKeychain('abcd'),
1239-
/Invalid commonKeychain length, expected 64 \(MPCv2\) or 128 \(MPCv1\), got 4/
1233+
/Invalid commonKeychain length, expected 128, got 4/
12401234
);
12411235
});
12421236
});

modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/createKeychains.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
201201
.once()
202202
.reply(200, {
203203
sessionId: 'test-session-id',
204-
commonPublicKey: 'a'.repeat(64),
204+
commonPublicKeychain: 'a'.repeat(128),
205205
bitgoMsg2: {
206206
message: Buffer.from('garbage').toString('base64'),
207207
signature: '-----BEGIN PGP SIGNATURE-----\nFAKE\n-----END PGP SIGNATURE-----',
@@ -221,7 +221,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
221221
.once()
222222
.reply(200, {
223223
sessionId: 'different-session-id',
224-
commonPublicKey: 'a'.repeat(64),
224+
commonPublicKeychain: 'a'.repeat(128),
225225
bitgoMsg2: { message: '', signature: '' },
226226
});
227227

@@ -231,7 +231,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
231231
);
232232
});
233233

234-
it('should reject when commonPublicKey from BitGo does not match the locally computed key', async function () {
234+
it('should reject when commonPublicKeychain from BitGo does not match the locally computed keychain', async function () {
235235
const bitgoSession = new EddsaMPSDkg.DKG(3, 2, 2);
236236
const bitgoState: { msg2?: MPSTypes.DeserializedMessage } = {};
237237
await nockMPSKeyGenRound1(bitgoSession, bitgoState, 1);
@@ -255,14 +255,14 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
255255

256256
return {
257257
sessionId: 'test-session-id',
258-
commonPublicKey: 'fakefakeee'.repeat(8), // mutated — will not match user/backup computed key
258+
commonPublicKeychain: 'fakefakeee'.repeat(16), // mutated — will not match user/backup computed keychain
259259
bitgoMsg2: await MPSComms.detachSignMpsMessage(Buffer.from(bitgoState.msg2.payload), bitgoPrvKeyObj),
260260
};
261261
});
262262

263263
await assert.rejects(
264264
tssUtils.createKeychains({ passphrase: 'test', enterprise: enterpriseId }),
265-
/does not match BitGo common public key/
265+
/does not match BitGo common keychain/
266266
);
267267
});
268268

@@ -286,6 +286,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
286286
name: 'irrelevant',
287287
publicKey: bitgoGpgKeyPair.publicKey,
288288
mpcv2PublicKey: bitgoGpgKeyPair.publicKey,
289+
eddsaMpcv2PublicKey: bitgoGpgKeyPair.publicKey,
289290
enterpriseId,
290291
});
291292
nock(stagingBgUrl).get('/api/v1/client/constants').reply(200, { ttl: 3600, constants });
@@ -390,7 +391,7 @@ describe('TSS EdDSA MPCv2 Utils:', async function () {
390391

391392
return {
392393
sessionId,
393-
commonPublicKey: bitgoSession.getSharePublicKey().toString('hex'),
394+
commonPublicKeychain: bitgoSession.getCommonKeychain(),
394395
bitgoMsg2: await MPSComms.detachSignMpsMessage(Buffer.from(bitgoState.msg2.payload), bitgoPrvKeyObj),
395396
};
396397
});

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

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,15 @@ export class BaseEddsaUtils extends baseTSSUtils<KeyShare> {
1414
/**
1515
* Get the commonPub portion of an EdDSA commonKeychain.
1616
*
17-
* MPCv1 keychains are 128 hex chars (32-byte public key + 32-byte chaincode).
18-
* MPCv2 keychains are 64 hex chars (32-byte public key only — no chaincode).
17+
* Keychains are 128 hex chars: 32-byte public key + 32-byte chaincode.
1918
*
2019
* @param {string} commonKeychain
2120
* @returns {string} base58-encoded public key
2221
*/
2322
static getPublicKeyFromCommonKeychain(commonKeychain: string): string {
24-
if (commonKeychain.length !== 64 && commonKeychain.length !== 128) {
25-
throw new Error(
26-
`Invalid commonKeychain length, expected 64 (MPCv2) or 128 (MPCv1), got ${commonKeychain.length}`
27-
);
23+
if (commonKeychain.length !== 128) {
24+
throw new Error(`Invalid commonKeychain length, expected 128, got ${commonKeychain.length}`);
2825
}
29-
// For MPCv1 (128 chars): the first 64 hex chars are the 32-byte public key.
30-
// For MPCv2 (64 chars): the entire string is the 32-byte public key.
3126
const pubHex = commonKeychain.slice(0, 64);
3227
return bs58.encode(Buffer.from(pubHex, 'hex'));
3328
}

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
9797

9898
const {
9999
sessionId: sessionIdRound2,
100-
commonPublicKeychain: commonPublicKey,
100+
commonPublicKeychain,
101101
bitgoMsg2,
102102
} = await this.sendKeyGenerationRound2(params.enterprise, {
103103
sessionId,
@@ -123,32 +123,40 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
123123
assert(userFinalMsgs.length === 0, 'WASM round 2 should produce no output messages for user');
124124
assert(backupFinalMsgs.length === 0, 'WASM round 2 should produce no output messages for backup');
125125

126-
const userCommonKey = userDkg.getSharePublicKey().toString('hex');
127-
const backupCommonKey = backupDkg.getSharePublicKey().toString('hex');
126+
const userCommonKeychain = userDkg.getCommonKeychain();
127+
const backupCommonKeychain = backupDkg.getCommonKeychain();
128128

129-
assert.equal(userCommonKey, commonPublicKey, 'User computed public key does not match BitGo common public key');
130-
assert.equal(backupCommonKey, commonPublicKey, 'Backup computed public key does not match BitGo common public key');
129+
assert.equal(
130+
userCommonKeychain,
131+
commonPublicKeychain,
132+
'User computed keychain does not match BitGo common keychain'
133+
);
134+
assert.equal(
135+
backupCommonKeychain,
136+
commonPublicKeychain,
137+
'Backup computed keychain does not match BitGo common keychain'
138+
);
131139

132140
const userPrivateMaterial = userDkg.getKeyShare();
133141
const backupPrivateMaterial = backupDkg.getKeyShare();
134142
const userReducedPrivateMaterial = userDkg.getReducedKeyShare();
135143
const backupReducedPrivateMaterial = backupDkg.getReducedKeyShare();
136144

137145
const userKeychainPromise = this.addUserKeychain(
138-
commonPublicKey,
146+
userCommonKeychain,
139147
userPrivateMaterial,
140148
userReducedPrivateMaterial,
141149
params.passphrase,
142150
params.originalPasscodeEncryptionCode
143151
);
144152
const backupKeychainPromise = this.addBackupKeychain(
145-
commonPublicKey,
153+
backupCommonKeychain,
146154
backupPrivateMaterial,
147155
backupReducedPrivateMaterial,
148156
params.passphrase,
149157
params.originalPasscodeEncryptionCode
150158
);
151-
const bitgoKeychainPromise = this.addBitgoKeychain(commonPublicKey);
159+
const bitgoKeychainPromise = this.addBitgoKeychain(userCommonKeychain);
152160

153161
const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([
154162
userKeychainPromise,

modules/sdk-lib-mpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
]
3737
},
3838
"dependencies": {
39-
"@bitgo/wasm-mps": "1.6.0",
39+
"@bitgo/wasm-mps": "1.7.0",
4040
"@noble/curves": "1.8.1",
4141
"@silencelaboratories/dkls-wasm-ll-node": "1.2.0-pre.4",
4242
"@silencelaboratories/dkls-wasm-ll-web": "1.2.0-pre.4",

modules/sdk-lib-mpc/src/tss/eddsa-mps/dkg.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export class DKG {
3636
private keyShare: Buffer | null = null;
3737
/** 32-byte Ed25519 public key from round2 */
3838
private sharePk: Buffer | null = null;
39+
/** 32-byte chain code from round2 */
40+
private shareChaincode: Buffer | null = null;
3941

4042
protected dkgState: DkgState = DkgState.Uninitialized;
4143

@@ -153,6 +155,7 @@ export class DKG {
153155
}
154156
this.keyShare = Buffer.from(share.share);
155157
this.sharePk = Buffer.from(share.pk);
158+
this.shareChaincode = Buffer.from(share.chaincode);
156159
this.dkgStateBytes = null;
157160
this.dkgState = DkgState.Complete;
158161
return [];
@@ -182,10 +185,19 @@ export class DKG {
182185
return this.sharePk;
183186
}
184187

188+
/**
189+
* Returns the 128-char hex common keychain: 64-char public key + 64-char chain code.
190+
* This matches the format expected by address derivation (Eddsa.deriveUnhardened).
191+
*/
192+
getCommonKeychain(): string {
193+
if (!this.sharePk || !this.shareChaincode) {
194+
throw Error('DKG session not initialized');
195+
}
196+
return this.sharePk.toString('hex') + this.shareChaincode.toString('hex');
197+
}
198+
185199
/**
186200
* Returns a CBOR-encoded reduced representation containing the public key.
187-
* Note: private key material and chain code are not separately accessible
188-
* from @bitgo/wasm-mps; the full keyshare is available via getKeyShare().
189201
*/
190202
getReducedKeyShare(): Buffer {
191203
if (!this.sharePk) {

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,10 +1015,10 @@
10151015
resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.7.0.tgz"
10161016
integrity sha512-KoXavJvyDHlEN+sWcigbgxYJtdFaU7gS0EkYQbNH4npVjNlzo6rL6gwjyWbyOy7oEs65DhpJ9vY5kRbE/bKiTQ==
10171017

1018-
"@bitgo/wasm-mps@1.6.0":
1019-
version "1.6.0"
1020-
resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.6.0.tgz#3e1f0618c1efac35ccd56301f8198f19d934e5ed"
1021-
integrity sha512-4Mzs124Wj3QbqaZqTYX4t2vSVNKblL/53SQFddoPgggfCnZpuV4tYovpD2sIwhbWe8hVWJXZR2/1CP+zUHKMaw==
1018+
"@bitgo/wasm-mps@1.7.0":
1019+
version "1.7.0"
1020+
resolved "https://registry.npmjs.org/@bitgo/wasm-mps/-/wasm-mps-1.7.0.tgz#e7ebca1afd2df757e69c5cdac702d6a06156867c"
1021+
integrity sha512-SNO7as4UvnE2ptDXp1oUXjABA8Y3/71lgVpAQyAGSfSaURjz4rG19+JZR54GBRIaA6hvUPr029b4gFyqoZPcgg==
10221022

10231023
"@bitgo/wasm-solana@^2.6.0":
10241024
version "2.6.0"

0 commit comments

Comments
 (0)