Skip to content

Commit 69370d7

Browse files
committed
feat(ebe): added EBE api for mpc v2
1 parent 2e67bdd commit 69370d7

File tree

5 files changed

+17516
-34
lines changed

5 files changed

+17516
-34
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { DklsComms, DklsDkg, DklsTypes } from '@bitgo-beta/sdk-lib-mpc';
2+
import {
3+
EnclavedApiSpecRouteRequest,
4+
MpcV2FinalizeResponseType,
5+
MpcV2RoundState,
6+
} from '../../../enclavedBitgoExpress/routers/enclavedApiSpec';
7+
import { KmsClient } from '../../../kms/kmsClient';
8+
import assert from 'assert';
9+
import { MPCv2PartiesEnum } from '@bitgo/sdk-core/dist/src/bitgo/utils/tss/ecdsa';
10+
11+
export async function mpcV2Finalize(
12+
req: EnclavedApiSpecRouteRequest<'v1.mpcv2.finalize', 'post'>,
13+
): Promise<MpcV2FinalizeResponseType> {
14+
const { source, encryptedData, encryptedDataKey, broadcastMessages, bitgoCommonKeychain } =
15+
req.decoded;
16+
17+
// setup clients
18+
const kms = new KmsClient(req.config);
19+
20+
// fetch previous state of execution
21+
const { plaintextKey } = await kms.decryptDataKey({ encryptedKey: encryptedDataKey });
22+
const state: MpcV2RoundState = JSON.parse(
23+
req.bitgo.decrypt({
24+
input: encryptedData,
25+
password: plaintextKey,
26+
}),
27+
);
28+
if (!state.bitgoGpgPub || !state.counterPartyGpgPub) {
29+
throw new Error('BitGo GPG public key or counterparty GPG public key is missing in state');
30+
}
31+
const { sessionData, sourceGpgPrv, bitgoGpgPub, counterPartyGpgPub } = state;
32+
33+
// restore session data and cast necessary fields into Uint8Array
34+
if (!sessionData) {
35+
throw new Error('Session data is missing for finalization');
36+
}
37+
sessionData.dkgSessionBytes = new Uint8Array(Object.values(sessionData.dkgSessionBytes));
38+
const session = await DklsDkg.Dkg.restoreSession(
39+
3,
40+
2,
41+
source === 'user' ? MPCv2PartiesEnum.USER : MPCv2PartiesEnum.BACKUP,
42+
sessionData,
43+
);
44+
45+
// processing incoming messages
46+
const incomingMessages = await DklsComms.decryptAndVerifyIncomingMessages(
47+
{
48+
broadcastMessages: Object.values(broadcastMessages),
49+
p2pMessages: [],
50+
},
51+
[bitgoGpgPub, counterPartyGpgPub],
52+
[sourceGpgPrv],
53+
);
54+
const deserializedIncomingMessages = DklsTypes.deserializeMessages(incomingMessages);
55+
session.handleIncomingMessages(deserializedIncomingMessages);
56+
57+
// get the common keychain
58+
const privateMaterial = session.getKeyShare();
59+
const commonKeychain = DklsTypes.getCommonKeychain(privateMaterial);
60+
61+
// verify the common keychain matches the Bitgo Common keychain
62+
assert.equal(
63+
bitgoCommonKeychain,
64+
commonKeychain,
65+
'Source and Bitgo Common keychains do not match',
66+
);
67+
68+
return {
69+
source,
70+
commonKeychain,
71+
};
72+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
EnclavedApiSpecRouteRequest,
3+
MpcV2InitializeResponseType,
4+
MpcV2RoundState,
5+
} from '../../../enclavedBitgoExpress/routers/enclavedApiSpec';
6+
import { KmsClient } from '../../../kms/kmsClient';
7+
import * as bitgoSdk from '@bitgo/sdk-core';
8+
import logger from '../../../logger';
9+
import { MPCv2PartiesEnum } from '@bitgo/sdk-core/dist/src/bitgo/utils/tss/ecdsa';
10+
11+
export async function mpcV2Initialize(
12+
req: EnclavedApiSpecRouteRequest<'v1.mpcv2.initialize', 'post'>,
13+
): Promise<MpcV2InitializeResponseType> {
14+
const { source } = req.decoded;
15+
16+
// setup clients
17+
const kms = new KmsClient(req.config);
18+
19+
// generate keys required
20+
const sourceGpgKey = await bitgoSdk.generateGPGKeyPair('secp256k1');
21+
const { plaintextKey, encryptedKey } = await kms.generateDataKey({ keyType: 'AES-256' });
22+
23+
// store the state of execution
24+
const state: MpcV2RoundState = {
25+
round: 1,
26+
sourceGpgPrv: {
27+
gpgKey: sourceGpgKey.privateKey,
28+
partyId: source === 'user' ? MPCv2PartiesEnum.USER : MPCv2PartiesEnum.BACKUP,
29+
},
30+
};
31+
32+
try {
33+
// Encrypt the state with the plaintext key
34+
const encryptedData = req.bitgo.encrypt({
35+
input: JSON.stringify(state),
36+
password: plaintextKey,
37+
});
38+
39+
return {
40+
gpgPub: sourceGpgKey.publicKey,
41+
encryptedDataKey: encryptedKey,
42+
encryptedData,
43+
};
44+
} catch (error) {
45+
logger.debug('Failed to initialize mpc key generation', error);
46+
console.error('Encryption error details:', error);
47+
throw error;
48+
}
49+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { DklsComms, DklsDkg, DklsTypes } from '@bitgo-beta/sdk-lib-mpc';
2+
import {
3+
EnclavedApiSpecRouteRequest,
4+
MpcV2RoundResponseType,
5+
MpcV2RoundState,
6+
} from '../../../enclavedBitgoExpress/routers/enclavedApiSpec';
7+
import { MPCv2PartiesEnum } from '@bitgo/sdk-core/dist/src/bitgo/utils/tss/ecdsa';
8+
import { KmsClient } from '../../../kms/kmsClient';
9+
10+
export async function mpcV2Round(
11+
req: EnclavedApiSpecRouteRequest<'v1.mpcv2.round', 'post'>,
12+
): Promise<MpcV2RoundResponseType> {
13+
const { source, encryptedData, encryptedDataKey, round, broadcastMessages, p2pMessages } =
14+
req.decoded;
15+
const bitgoGpgPubInput = req.body.bitgoGpgPub;
16+
const counterPartyGpgPubInput = req.body.counterPartyGpgPub;
17+
18+
// setup clients
19+
const kms = new KmsClient(req.config);
20+
21+
// sanity checks
22+
if (round < 1 || round > 4) {
23+
throw new Error('Round must be between 1 and 4');
24+
}
25+
26+
if (!broadcastMessages && !p2pMessages && round > 1) {
27+
throw new Error('At least one of broadcastMessages or p2pMessages must be provided');
28+
}
29+
30+
if (broadcastMessages && Object.keys(broadcastMessages).length === 0) {
31+
throw new Error('broadcastMessages did not contain all required messages');
32+
}
33+
34+
if (p2pMessages && Object.keys(p2pMessages).length === 0) {
35+
throw new Error('p2pMessages did not contain all required messages');
36+
}
37+
38+
// fetch previous state of execution
39+
const { plaintextKey } = await kms.decryptDataKey({ encryptedKey: encryptedDataKey });
40+
const state: MpcV2RoundState = JSON.parse(
41+
req.bitgo.decrypt({
42+
input: encryptedData,
43+
password: plaintextKey,
44+
}),
45+
);
46+
47+
// sanity checks against previous state and set GPG pub keys in state
48+
if (!state.bitgoGpgPub) {
49+
state.bitgoGpgPub = {
50+
gpgKey: bitgoGpgPubInput,
51+
partyId: MPCv2PartiesEnum.BITGO,
52+
};
53+
} else if (bitgoGpgPubInput && state.bitgoGpgPub.gpgKey !== bitgoGpgPubInput) {
54+
throw new Error(
55+
`BitGo GPG public key mismatch: expected ${state.bitgoGpgPub}, got ${bitgoGpgPubInput}`,
56+
);
57+
}
58+
59+
if (!state.counterPartyGpgPub) {
60+
state.counterPartyGpgPub = {
61+
gpgKey: counterPartyGpgPubInput,
62+
partyId: source === 'user' ? MPCv2PartiesEnum.BACKUP : MPCv2PartiesEnum.USER,
63+
};
64+
} else if (
65+
counterPartyGpgPubInput &&
66+
state.counterPartyGpgPub.gpgKey !== counterPartyGpgPubInput
67+
) {
68+
throw new Error(
69+
`Counterparty GPG public key mismatch: expected ${state.counterPartyGpgPub}, got ${counterPartyGpgPubInput}`,
70+
);
71+
}
72+
73+
if (state.round !== round) {
74+
throw new Error(`Round mismatch: expected ${state.round}, got ${round}`);
75+
}
76+
const { sourceGpgPrv, bitgoGpgPub, counterPartyGpgPub, sessionData } = state;
77+
78+
// restore session data and cast necessary fields into Uint8Array
79+
if (!sessionData && round > 1) {
80+
throw new Error('Session data is missing for round greater than 1');
81+
} else if (sessionData) {
82+
sessionData.dkgSessionBytes = new Uint8Array(Object.values(sessionData.dkgSessionBytes));
83+
sessionData.chainCodeCommitment = new Uint8Array(
84+
Object.values(sessionData.chainCodeCommitment || {}),
85+
);
86+
}
87+
const session =
88+
round === 1
89+
? new DklsDkg.Dkg(3, 2, source === 'user' ? MPCv2PartiesEnum.USER : MPCv2PartiesEnum.BACKUP)
90+
: await DklsDkg.Dkg.restoreSession(
91+
3,
92+
2,
93+
source === 'user' ? MPCv2PartiesEnum.USER : MPCv2PartiesEnum.BACKUP,
94+
sessionData as DklsDkg.DkgSessionData,
95+
);
96+
97+
// decrypt incoming messages and handle them to form outgoing messages
98+
let outgoingMessages: DklsTypes.DeserializedMessages = { broadcastMessages: [], p2pMessages: [] };
99+
if (round === 1) {
100+
outgoingMessages.broadcastMessages = [await session.initDkg()];
101+
} else {
102+
// decrypt messages, they should be auth by bitgoGpgPub, counterPartyGpgPub; and decrypt by sourceGpgPrv
103+
const incomingMessages = await DklsComms.decryptAndVerifyIncomingMessages(
104+
{
105+
p2pMessages: Object.values(p2pMessages || {}),
106+
broadcastMessages: Object.values(broadcastMessages || {}),
107+
},
108+
[bitgoGpgPub, counterPartyGpgPub],
109+
[sourceGpgPrv],
110+
);
111+
112+
const deserializedIncomingMessages = DklsTypes.deserializeMessages(incomingMessages);
113+
114+
// generate outgoing messages based on incoming messages
115+
try {
116+
outgoingMessages = session.handleIncomingMessages(deserializedIncomingMessages);
117+
} catch (error: any) {
118+
console.error('Error handling incoming messages:', error);
119+
throw new Error(`Failed to handle incoming messages: ${error.message}`);
120+
}
121+
}
122+
123+
// cast outgoing messages commitment to Uint8Array if not already
124+
outgoingMessages.p2pMessages = outgoingMessages.p2pMessages.map((msg) => {
125+
if (!(msg.commitment instanceof Uint8Array))
126+
return { ...msg, commitment: new Uint8Array(Object.values(msg.commitment as any)) };
127+
return msg;
128+
});
129+
130+
// sign and encrypt outgoing messages
131+
const serializedOutgoingMessages = DklsTypes.serializeMessages(outgoingMessages);
132+
const signedMessages = await DklsComms.encryptAndAuthOutgoingMessages(
133+
serializedOutgoingMessages,
134+
[bitgoGpgPub, counterPartyGpgPub],
135+
[sourceGpgPrv],
136+
);
137+
138+
// re-encrypt state
139+
let newEncryptedData;
140+
try {
141+
newEncryptedData = req.bitgo.encrypt({
142+
input: JSON.stringify({
143+
...state,
144+
round: state.round + 1,
145+
sessionData: session.getSessionData(),
146+
}),
147+
password: plaintextKey,
148+
});
149+
} catch (error) {
150+
console.error('Encryption error details:', error);
151+
throw error;
152+
}
153+
154+
return {
155+
round: state.round + 1,
156+
encryptedDataKey,
157+
encryptedData: newEncryptedData,
158+
p2pMessages:
159+
signedMessages.p2pMessages.length > 0
160+
? {
161+
bitgo: signedMessages.p2pMessages.find((msg) => msg.to === MPCv2PartiesEnum.BITGO),
162+
counterParty: signedMessages.p2pMessages.find(
163+
(msg) => msg.to !== MPCv2PartiesEnum.BITGO,
164+
),
165+
}
166+
: undefined,
167+
broadcastMessage:
168+
signedMessages.broadcastMessages.length > 0 ? signedMessages.broadcastMessages[0] : undefined,
169+
};
170+
}

0 commit comments

Comments
 (0)