Repository navigation
feat: add eddsa mpcv2 custom signers in master express #279
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
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| import 'should'; | ||
|
|
||
| import { TxRequest } from '@bitgo-beta/sdk-core'; | ||
| import { | ||
| AdvancedWalletManagerClient, | ||
| SignMpcV2Round1Response, | ||
| SignMpcV2Round2Response, | ||
| } from '../../../masterBitgoExpress/clients/advancedWalletManagerClient'; | ||
| import { createEddsaMPCv2CustomSigners } from '../../../masterBitgoExpress/handlers/eddsa'; | ||
|
|
||
| describe('createEddsaMPCv2CustomSigners', () => { | ||
| const source = 'user' as const; | ||
| const commonKeychain = 'mock-common-keychain'; | ||
|
|
||
| const txRequestFixture = { txRequestId: '123456' } as unknown as TxRequest; // minimal fixture: generators forward txRequest opaquely | ||
|
|
||
| const mockRound1Response: SignMpcV2Round1Response = { | ||
| signatureShareRound1: { from: 'user', to: 'bitgo', share: 'mock-round1-share' }, | ||
| userGpgPubKey: 'mock-user-gpg-pub-key', | ||
| encryptedRound1Session: 'mock-encrypted-round1-session', | ||
| encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', | ||
| encryptedDataKey: 'mock-encrypted-data-key', | ||
| }; | ||
| const mockRound2Response: SignMpcV2Round2Response = { | ||
| signatureShareRound2: { from: 'user', to: 'bitgo', share: 'mock-round2-share' }, | ||
| encryptedRound2Session: 'mock-encrypted-round2-session', | ||
| }; | ||
| const mockRound3Response = { | ||
| signatureShareRound3: { from: 'user', to: 'bitgo', share: 'mock-round3-share' }, | ||
| }; | ||
|
|
||
| type AwmCall = { | ||
| method: string; | ||
| source: string; | ||
| commonKeychain: string; | ||
| params: Record<string, unknown>; | ||
| }; | ||
|
|
||
| let awmCalls: AwmCall[]; | ||
| let awmClient: AdvancedWalletManagerClient; | ||
|
|
||
| beforeEach(() => { | ||
| awmCalls = []; | ||
| // Minimal stub: only the three MPCv2 sign methods are exercised here. | ||
| awmClient = { | ||
| signMPCv2Round1: async ( | ||
| source: 'user' | 'backup', | ||
| commonKeychain: string, | ||
| params: Record<string, unknown>, | ||
| ) => { | ||
| awmCalls.push({ method: 'signMPCv2Round1', source, commonKeychain, params }); | ||
| return mockRound1Response; | ||
| }, | ||
| signMPCv2Round2: async ( | ||
| source: 'user' | 'backup', | ||
| commonKeychain: string, | ||
| params: Record<string, unknown>, | ||
| ) => { | ||
| awmCalls.push({ method: 'signMPCv2Round2', source, commonKeychain, params }); | ||
| return mockRound2Response; | ||
| }, | ||
| signMPCv2Round3: async ( | ||
| source: 'user' | 'backup', | ||
| commonKeychain: string, | ||
| params: Record<string, unknown>, | ||
| ) => { | ||
| awmCalls.push({ method: 'signMPCv2Round3', source, commonKeychain, params }); | ||
| return mockRound3Response; | ||
| }, | ||
| } as unknown as AdvancedWalletManagerClient; | ||
| }); | ||
|
|
||
| it('round 1 generator calls AWM with curveType eddsa', async () => { | ||
| const { customEddsaMPCv2Round1Generator } = createEddsaMPCv2CustomSigners( | ||
| awmClient, | ||
| source, | ||
| commonKeychain, | ||
| ); | ||
|
|
||
| const response = await customEddsaMPCv2Round1Generator({ txRequest: txRequestFixture }); | ||
|
|
||
| response.should.equal(mockRound1Response); | ||
| awmCalls.should.have.length(1); | ||
| awmCalls[0].method.should.equal('signMPCv2Round1'); | ||
| awmCalls[0].source.should.equal(source); | ||
| awmCalls[0].commonKeychain.should.equal(commonKeychain); | ||
| awmCalls[0].params.should.have.property('curveType', 'eddsa'); | ||
| awmCalls[0].params.should.have.property('txRequest', txRequestFixture); | ||
| }); | ||
|
|
||
| it('round 2 generator forwards round 1 encrypted state and curveType eddsa', async () => { | ||
| const { customEddsaMPCv2Round1Generator, customEddsaMPCv2Round2Generator } = | ||
| createEddsaMPCv2CustomSigners(awmClient, source, commonKeychain); | ||
|
|
||
| await customEddsaMPCv2Round1Generator({ txRequest: txRequestFixture }); | ||
| const response = await customEddsaMPCv2Round2Generator({ | ||
| txRequest: txRequestFixture, | ||
| bitgoPublicGpgKey: 'mock-bitgo-gpg-key', | ||
| encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', | ||
| encryptedRound1Session: 'mock-encrypted-round1-session', | ||
| }); | ||
|
|
||
| response.should.equal(mockRound2Response); | ||
| awmCalls.should.have.length(2); | ||
| awmCalls[1].method.should.equal('signMPCv2Round2'); | ||
| awmCalls[1].params.should.have.property('curveType', 'eddsa'); | ||
| awmCalls[1].params.should.have.property( | ||
| 'encryptedDataKey', | ||
| mockRound1Response.encryptedDataKey, | ||
| ); | ||
| awmCalls[1].params.should.have.property( | ||
| 'encryptedRound1Session', | ||
| mockRound1Response.encryptedRound1Session, | ||
| ); | ||
| awmCalls[1].params.should.have.property( | ||
| 'encryptedUserGpgPrvKey', | ||
| mockRound1Response.encryptedUserGpgPrvKey, | ||
| ); | ||
| awmCalls[1].params.should.have.property('bitgoPublicGpgKey', 'mock-bitgo-gpg-key'); | ||
| }); | ||
|
|
||
| it('round 3 generator forwards round 1 + round 2 encrypted state and curveType eddsa', async () => { | ||
| const { | ||
| customEddsaMPCv2Round1Generator, | ||
| customEddsaMPCv2Round2Generator, | ||
| customEddsaMPCv2Round3Generator, | ||
| } = createEddsaMPCv2CustomSigners(awmClient, source, commonKeychain); | ||
|
|
||
| await customEddsaMPCv2Round1Generator({ txRequest: txRequestFixture }); | ||
| await customEddsaMPCv2Round2Generator({ | ||
| txRequest: txRequestFixture, | ||
| bitgoPublicGpgKey: 'mock-bitgo-gpg-key', | ||
| encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', | ||
| encryptedRound1Session: 'mock-encrypted-round1-session', | ||
| }); | ||
| const response = await customEddsaMPCv2Round3Generator({ | ||
| txRequest: txRequestFixture, | ||
| bitgoPublicGpgKey: 'mock-bitgo-gpg-key', | ||
| encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', | ||
| encryptedRound2Session: 'mock-encrypted-round2-session', | ||
| }); | ||
|
|
||
| response.should.equal(mockRound3Response); | ||
| awmCalls.should.have.length(3); | ||
| awmCalls[2].method.should.equal('signMPCv2Round3'); | ||
| awmCalls[2].params.should.have.property('curveType', 'eddsa'); | ||
| awmCalls[2].params.should.have.property( | ||
| 'encryptedDataKey', | ||
| mockRound1Response.encryptedDataKey, | ||
| ); | ||
| awmCalls[2].params.should.have.property( | ||
| 'encryptedRound2Session', | ||
| mockRound2Response.encryptedRound2Session, | ||
| ); | ||
| awmCalls[2].params.should.have.property( | ||
| 'encryptedUserGpgPrvKey', | ||
| mockRound1Response.encryptedUserGpgPrvKey, | ||
| ); | ||
| }); | ||
|
|
||
| it('round 2 before round 1 throws the expected error', async () => { | ||
| const { customEddsaMPCv2Round2Generator } = createEddsaMPCv2CustomSigners( | ||
| awmClient, | ||
| source, | ||
| commonKeychain, | ||
| ); | ||
|
|
||
| await customEddsaMPCv2Round2Generator({ | ||
| txRequest: txRequestFixture, | ||
| bitgoPublicGpgKey: 'mock-bitgo-gpg-key', | ||
| encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', | ||
| encryptedRound1Session: 'mock-encrypted-round1-session', | ||
| }).should.be.rejectedWith('Round 1 must be completed before Round 2'); | ||
| awmCalls.should.have.length(0); | ||
| }); | ||
|
|
||
| it('round 3 before round 2 throws the expected error', async () => { | ||
| const { customEddsaMPCv2Round1Generator, customEddsaMPCv2Round3Generator } = | ||
| createEddsaMPCv2CustomSigners(awmClient, source, commonKeychain); | ||
|
|
||
| await customEddsaMPCv2Round1Generator({ txRequest: txRequestFixture }); | ||
| await customEddsaMPCv2Round3Generator({ | ||
| txRequest: txRequestFixture, | ||
| bitgoPublicGpgKey: 'mock-bitgo-gpg-key', | ||
| encryptedUserGpgPrvKey: 'mock-encrypted-user-gpg-prv-key', | ||
| encryptedRound2Session: 'mock-encrypted-round2-session', | ||
| }).should.be.rejectedWith('Round 2 must be completed before Round 3'); | ||
| awmCalls.should.have.length(1); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
🟡 Medium — Test fixtures can't prove captured-state forwarding. In
eddsaMpcv2Signing.test.ts, the caller-supplied values are byte-identical to the captured-response values:encryptedRound1Session: 'mock-encrypted-round1-session'(fixture line 20 vs test input lines 100/134/172),encryptedUserGpgPrvKey(line 21 vs 99/133/139/171/185),encryptedRound2Session(line 26 vs 140/186). So assertions like line 112-113 pass even if the implementation forwarded the caller's param instead of the captured round-1 response — the PR's headline behavior isn't pinned. (encryptedDataKeyis genuinely tested, since callers can't supply it.) Fix: use distinct strings, e.g. caller passes'caller-round1-session'and assert the AWM param equals'mock-encrypted-round1-session'.Non-blocking — fine to address in a follow-up.