-
Notifications
You must be signed in to change notification settings - Fork 303
feat: add eddsa MPCv2 SMC utils #8845
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,163 @@ | ||
| import assert from 'assert'; | ||
| import { | ||
| EddsaBitgoToOVC1Round1Response, | ||
| EddsaBitgoToOVC1Round2Response, | ||
| EddsaKeyCreationMPCv2StateEnum, | ||
| EddsaMPCv2KeyGenRound1Response, | ||
| EddsaMPCv2KeyGenRound2Response, | ||
| EddsaOVC1ToBitgoRound1Payload, | ||
| EddsaOVC2ToBitgoRound2Payload, | ||
| OVCIndexEnum, | ||
| } from '@bitgo/public-types'; | ||
| import { IBaseCoin } from '../../../../baseCoin'; | ||
| import { BitGoBase } from '../../../../bitgoBase'; | ||
| import { decodeOrElse, Keychain } from '../../../..'; | ||
| import { EddsaMPCv2Utils } from '../eddsaMPCv2'; | ||
| import { EddsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from '../eddsaMPCv2KeyGenSender'; | ||
|
|
||
| export class MPCv2SMCUtils { | ||
| private MPCv2Utils: EddsaMPCv2Utils; | ||
|
|
||
| constructor(private bitgo: BitGoBase, private baseCoin: IBaseCoin) { | ||
| this.MPCv2Utils = new EddsaMPCv2Utils(bitgo, baseCoin); | ||
| } | ||
|
|
||
| public async keyGenRound1( | ||
| enterprise: string, | ||
| payload: EddsaOVC1ToBitgoRound1Payload | ||
| ): Promise<EddsaBitgoToOVC1Round1Response> { | ||
| return this.keyGenRound1BySender(KeyGenSenderForEnterprise(this.bitgo, enterprise), payload); | ||
| } | ||
|
|
||
| public async keyGenRound2( | ||
| enterprise: string, | ||
| payload: EddsaOVC2ToBitgoRound2Payload | ||
| ): Promise<EddsaBitgoToOVC1Round2Response> { | ||
| return this.keyGenRound2BySender(KeyGenSenderForEnterprise(this.bitgo, enterprise), payload); | ||
| } | ||
|
|
||
| public async keyGenRound1BySender( | ||
| senderFn: EddsaMPCv2KeyGenSendFn<EddsaMPCv2KeyGenRound1Response>, | ||
| payload: EddsaOVC1ToBitgoRound1Payload | ||
| ): Promise<EddsaBitgoToOVC1Round1Response> { | ||
| assert( | ||
| payload.state === EddsaKeyCreationMPCv2StateEnum.WaitingForBitgoRound1Data, | ||
| `Invalid state for round 1, expected: ${EddsaKeyCreationMPCv2StateEnum.WaitingForBitgoRound1Data}, got: ${payload.state}` | ||
| ); | ||
| decodeOrElse(EddsaOVC1ToBitgoRound1Payload.name, EddsaOVC1ToBitgoRound1Payload, payload, (errors) => { | ||
| throw new Error(`error(s) parsing payload: ${errors}`); | ||
| }); | ||
|
|
||
| const ovc1 = payload.ovc[OVCIndexEnum.ONE]; | ||
| const ovc2 = payload.ovc[OVCIndexEnum.TWO]; | ||
| const result = await this.MPCv2Utils.sendKeyGenerationRound1BySender(senderFn, { | ||
| userGpgPublicKey: ovc1.gpgPubKey, | ||
| backupGpgPublicKey: ovc2.gpgPubKey, | ||
| userMsg1: ovc1.ovcMsg1, | ||
| backupMsg1: ovc2.ovcMsg1, | ||
| }); | ||
|
|
||
| const response = { | ||
| state: EddsaKeyCreationMPCv2StateEnum.WaitingForOVC1Round2Data, | ||
| tssVersion: payload.tssVersion, | ||
| walletType: payload.walletType, | ||
| coin: payload.coin, | ||
| ovc: payload.ovc, | ||
| platform: { | ||
| walletGpgPubKeySigs: (result as EddsaMPCv2KeyGenRound1Response & { walletGpgPubKeySigs: string }) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Type gap on If the EdDSA round-1 endpoint returns this field, add it to |
||
| .walletGpgPubKeySigs, | ||
| sessionId: result.sessionId, | ||
| bitgoMsg1: result.bitgoMsg1, | ||
| }, | ||
| }; | ||
|
|
||
| return decodeOrElse(EddsaBitgoToOVC1Round1Response.name, EddsaBitgoToOVC1Round1Response, response, (errors) => { | ||
| throw new Error(`error(s) parsing response: ${errors}`); | ||
| }); | ||
| } | ||
|
|
||
| public async keyGenRound2BySender( | ||
| senderFn: EddsaMPCv2KeyGenSendFn<EddsaMPCv2KeyGenRound2Response>, | ||
| payload: EddsaOVC2ToBitgoRound2Payload | ||
| ): Promise<EddsaBitgoToOVC1Round2Response> { | ||
| assert( | ||
| payload.state === EddsaKeyCreationMPCv2StateEnum.WaitingForBitgoRound2Data, | ||
| `Invalid state for round 2, expected: ${EddsaKeyCreationMPCv2StateEnum.WaitingForBitgoRound2Data}, got: ${payload.state}` | ||
| ); | ||
| decodeOrElse(EddsaOVC2ToBitgoRound2Payload.name, EddsaOVC2ToBitgoRound2Payload, payload, (errors) => { | ||
| throw new Error(`error(s) parsing payload: ${errors}`); | ||
| }); | ||
|
|
||
| const ovc1 = payload.ovc[OVCIndexEnum.ONE]; | ||
| const ovc2 = payload.ovc[OVCIndexEnum.TWO]; | ||
| const sessionId = payload.platform.sessionId; | ||
| const result = await this.MPCv2Utils.sendKeyGenerationRound2BySender(senderFn, { | ||
| sessionId, | ||
| userMsg2: ovc1.ovcMsg2, | ||
| backupMsg2: ovc2.ovcMsg2, | ||
| }); | ||
|
|
||
| assert.equal(sessionId, result.sessionId, 'Round 1 and round 2 session IDs do not match'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pattern inconsistency with ECDSA for ECDSA platform: {
...payload.platform,
sessionId: result.sessionId, // updated from result
bitgoCommitment2: ...,
}This PR asserts equality ( These two choices are consistent with each other (assert equality, then propagate original), but it's unclear if the assertion is correct. If the EdDSA round 2 API returns a different Please add a comment documenting why EdDSA session IDs are stable across rounds (if that's the API contract), or switch to the ECDSA pattern and remove the assertion. |
||
|
|
||
| const keychains = this.baseCoin.keychains(); | ||
| const bitgoKeychain = await keychains.add({ | ||
| source: 'bitgo', | ||
| keyType: 'tss', | ||
| commonKeychain: result.commonPublicKeychain, | ||
| isMPCv2: true, | ||
| }); | ||
|
|
||
| const response = { | ||
| state: EddsaKeyCreationMPCv2StateEnum.WaitingForOVC1GenerateKey, | ||
| bitGoKeyId: bitgoKeychain.id, | ||
| tssVersion: payload.tssVersion, | ||
| walletType: payload.walletType, | ||
| coin: payload.coin, | ||
| ovc: payload.ovc, | ||
| platform: { | ||
| ...payload.platform, | ||
| commonPublicKeychain: result.commonPublicKeychain, | ||
| bitgoMsg2: result.bitgoMsg2, | ||
| }, | ||
| }; | ||
|
|
||
| return decodeOrElse(EddsaBitgoToOVC1Round2Response.name, EddsaBitgoToOVC1Round2Response, response, (errors) => { | ||
| throw new Error(`error(s) parsing response: ${errors}`); | ||
| }); | ||
| } | ||
|
|
||
| public async uploadClientKeys( | ||
| bitgoKeyId: string, | ||
| userCommonKeychain: string, | ||
| backupCommonKeychain: string | ||
| ): Promise<{ userKeychain: Keychain; backupKeychain: Keychain; bitgoKeychain: Keychain }> { | ||
| assert( | ||
| userCommonKeychain === backupCommonKeychain, | ||
| 'Common keychain mismatch between the user and backup keychains' | ||
| ); | ||
|
|
||
| const keychains = this.baseCoin.keychains(); | ||
| const bitgoKeychain = await keychains.get({ id: bitgoKeyId }); | ||
| assert(bitgoKeychain, 'Keychain not found'); | ||
| assert(bitgoKeychain.source === 'bitgo', 'The keychain is not a BitGo keychain'); | ||
| assert(bitgoKeychain.type === 'tss', 'BitGo keychain is not a TSS keychain'); | ||
| assert(bitgoKeychain.commonKeychain, 'BitGo keychain does not have a common keychain'); | ||
| assert(bitgoKeychain.commonKeychain === userCommonKeychain, 'Common keychain mismatch between the OVCs and BitGo'); | ||
|
|
||
| const userKeychainPromise = keychains.add({ | ||
| source: 'user', | ||
| keyType: 'tss', | ||
| commonKeychain: userCommonKeychain, | ||
| isMPCv2: true, | ||
| }); | ||
| const backupKeychainPromise = keychains.add({ | ||
| source: 'backup', | ||
| keyType: 'tss', | ||
| commonKeychain: backupCommonKeychain, | ||
| isMPCv2: true, | ||
| }); | ||
|
|
||
| const [userKeychain, backupKeychain] = await Promise.all([userKeychainPromise, backupKeychainPromise]); | ||
| return { userKeychain, backupKeychain, bitgoKeychain }; | ||
| } | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No unit tests added. At minimum: (1) a state-guard failure test for each of the two |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: unsafe
ascast hiding a type definition gap.EddsaMPCv2KeyGenRound1Responseis typed as{ sessionId, bitgoMsg1 }only —walletGpgPubKeySigsis not declared on it. The cast(result as EddsaMPCv2KeyGenRound1Response & { walletGpgPubKeySigs: string })works around this gap.For ECDSA,
MPCv2KeyGenRound1Responseproperly declareswalletGpgPubKeySigs: NonEmptyStringand is accessed directly without any cast.Two scenarios:
/mpc/keygen/round1API does returnwalletGpgPubKeySigs→EddsaMPCv2KeyGenRound1Responsein@bitgo/public-typesneeds to be updated to include the field, and this cast should be removed.walletGpgPubKeySigswill beundefinedat runtime. IfEddsaBitgoToOVC1Round1Response.platform.walletGpgPubKeySigsis a requiredNonEmptyString(as it is for the ECDSA equivalent),decodeOrElsewill throw"error(s) parsing response"on every call, makingkeyGenRound1BySenderpermanently broken.Either fix the type in
@bitgo/public-typesor removewalletGpgPubKeySigsfrom the platform construction if EdDSA doesn't use it.