Skip to content

Commit 3d581de

Browse files
committed
feat(sdk-core): add EdDSA MPCv2 DSG helpers and DKG key-share util
- Add getSignatureShareRoundOne/Two/Three helpers in sdk-core for building PGP-signed MPS broadcast messages per signing round - Add verifyBitGoMessageRoundOne/Two helpers for verifying peer PGP signatures and deserialising incoming MPS messages - Parameterise partyId (default: USER=0) and peerPartyId (type: MPCv2PartiesEnum, default: BITGO=2) to support non-user signers without hardcoding - Wire eddsaMpcV2 type in sendSignatureShareV2 (common.ts) - Add generateEdDsaDKGKeyShares to sdk-lib-mpc MPSUtil with split EdDsaDKGPartySeed (encKey / dkgSeed) to eliminate seed dual-use; add length assertions - Replace duplicate test-util copy with a re-export from the production source - Add unit tests covering all helpers including BACKUP party path, tampered-message rejection for both round-1 and round-2 verify, and deterministic-seed behaviour Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> TICKET: WCI-153
1 parent 18f9f86 commit 3d581de

8 files changed

Lines changed: 582 additions & 64 deletions

File tree

modules/sdk-core/src/bitgo/tss/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ export async function sendSignatureShareV2(
131131
let type = '';
132132
if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'ecdsa') {
133133
type = 'ecdsaMpcV2';
134+
} else if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'eddsa') {
135+
type = 'eddsaMpcV2';
134136
} else if (multisigTypeVersion === undefined && mpcAlgorithm === 'eddsa') {
135137
type = 'eddsaMpcV1';
136138
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import assert from 'assert';
2+
import * as openpgp from 'openpgp';
3+
import { MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc';
4+
import {
5+
EddsaMPCv2SignatureShareRound1Input,
6+
EddsaMPCv2SignatureShareRound1Output,
7+
EddsaMPCv2SignatureShareRound2Input,
8+
EddsaMPCv2SignatureShareRound2Output,
9+
EddsaMPCv2SignatureShareRound3Input,
10+
} from '@bitgo/public-types';
11+
import { SignatureShareRecord, SignatureShareType } from '../../utils/tss/baseTypes';
12+
import { MPCv2PartiesEnum } from '../../utils/tss/ecdsa/typesMPCv2';
13+
14+
function partyIdToSignatureShareType(partyId: 0 | 1 | 2): SignatureShareType {
15+
assert(partyId === 0 || partyId === 1 || partyId === 2, 'Invalid partyId for EdDSA MPCv2 signing');
16+
switch (partyId) {
17+
case 0:
18+
return SignatureShareType.USER;
19+
case 1:
20+
return SignatureShareType.BACKUP;
21+
case 2:
22+
return SignatureShareType.BITGO;
23+
}
24+
}
25+
26+
/**
27+
* Builds the round-1 signature share record.
28+
*
29+
* PGP-signs the WASM round-0 broadcast message with the signer's ephemeral key and
30+
* wraps it into a SignatureShareRecord ready for `sendSignatureShareV2`.
31+
*/
32+
export async function getSignatureShareRoundOne(
33+
userMsg1: MPSTypes.DeserializedMessage,
34+
userGpgPrivKey: openpgp.PrivateKey,
35+
partyId: 0 | 1 = 0,
36+
otherSignerPartyId: 0 | 1 | 2 = 2
37+
): Promise<SignatureShareRecord> {
38+
const signedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg1.payload), userGpgPrivKey);
39+
const share: EddsaMPCv2SignatureShareRound1Input = {
40+
type: 'round1Input',
41+
data: { msg1: signedMsg1 },
42+
};
43+
return {
44+
from: partyIdToSignatureShareType(partyId),
45+
to: partyIdToSignatureShareType(otherSignerPartyId),
46+
share: JSON.stringify(share),
47+
};
48+
}
49+
50+
/**
51+
* Verifies the peer's round-1 PGP signature and returns the raw deserialized
52+
* message ready for `DSG.handleIncomingMessages`.
53+
*/
54+
export async function verifyBitGoMessageRoundOne(
55+
parsedRound1Output: EddsaMPCv2SignatureShareRound1Output,
56+
peerGpgKey: openpgp.Key,
57+
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
58+
): Promise<MPSTypes.DeserializedMessage> {
59+
const rawBytes = await MPSComms.verifyMpsMessage(parsedRound1Output.data.msg1, peerGpgKey);
60+
return {
61+
from: peerPartyId,
62+
payload: new Uint8Array(rawBytes),
63+
};
64+
}
65+
66+
/**
67+
* Builds the round-2 signature share record.
68+
*/
69+
export async function getSignatureShareRoundTwo(
70+
userMsg2: MPSTypes.DeserializedMessage,
71+
userGpgPrivKey: openpgp.PrivateKey,
72+
partyId: 0 | 1 = 0,
73+
otherSignerPartyId: 0 | 1 | 2 = 2
74+
): Promise<SignatureShareRecord> {
75+
const signedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg2.payload), userGpgPrivKey);
76+
const share: EddsaMPCv2SignatureShareRound2Input = {
77+
type: 'round2Input',
78+
data: { msg2: signedMsg2 },
79+
};
80+
return {
81+
from: partyIdToSignatureShareType(partyId),
82+
to: partyIdToSignatureShareType(otherSignerPartyId),
83+
share: JSON.stringify(share),
84+
};
85+
}
86+
87+
/**
88+
* Verifies the peer's round-2 PGP signature and returns the raw deserialized
89+
* message ready for `DSG.handleIncomingMessages`.
90+
*/
91+
export async function verifyBitGoMessageRoundTwo(
92+
parsedRound2Output: EddsaMPCv2SignatureShareRound2Output,
93+
peerGpgKey: openpgp.Key,
94+
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
95+
): Promise<MPSTypes.DeserializedMessage> {
96+
const rawBytes = await MPSComms.verifyMpsMessage(parsedRound2Output.data.msg2, peerGpgKey);
97+
return {
98+
from: peerPartyId,
99+
payload: new Uint8Array(rawBytes),
100+
};
101+
}
102+
103+
/**
104+
* Builds the round-3 signature share record (final signer message).
105+
*
106+
* There is no corresponding `verifyBitGoMessageRoundThree` because Wallet Platform
107+
* finalises the signing server-side after receiving round 3; the client obtains the
108+
* signed transaction via `sendTxRequest`.
109+
*/
110+
export async function getSignatureShareRoundThree(
111+
userMsg3: MPSTypes.DeserializedMessage,
112+
userGpgPrivKey: openpgp.PrivateKey,
113+
partyId: 0 | 1 = 0,
114+
otherSignerPartyId: 0 | 1 | 2 = 2
115+
): Promise<SignatureShareRecord> {
116+
const signedMsg3 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg3.payload), userGpgPrivKey);
117+
const share: EddsaMPCv2SignatureShareRound3Input = {
118+
type: 'round3Input',
119+
data: { msg3: signedMsg3 },
120+
};
121+
return {
122+
from: partyIdToSignatureShareType(partyId),
123+
to: partyIdToSignatureShareType(otherSignerPartyId),
124+
share: JSON.stringify(share),
125+
};
126+
}

modules/sdk-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { EcdsaUtils } from './bitgo/utils/tss/ecdsa/ecdsa';
1010
export { EcdsaUtils };
1111
import { EcdsaMPCv2Utils } from './bitgo/utils/tss/ecdsa/ecdsaMPCv2';
1212
export { EcdsaMPCv2Utils };
13+
import { EddsaMPCv2Utils } from './bitgo/utils/tss/eddsa/eddsaMPCv2';
14+
export { EddsaMPCv2Utils };
1315
export { verifyEddsaTssWalletAddress, verifyMPCWalletAddress } from './bitgo/utils/tss/addressVerification';
1416
export { GShare, SignShare, YShare } from './account-lib/mpc/tss/eddsa/types';
1517
export { TssEcdsaStep1ReturnMessage, TssEcdsaStep2ReturnMessage } from './bitgo/tss/types';

0 commit comments

Comments
 (0)