-
Notifications
You must be signed in to change notification settings - Fork 307
feat(sdk-core): extract isMpcV2Keycard and signEddsaMpcV2RecoveryTx into shared utils #9449
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
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 |
|---|---|---|
|
|
@@ -72,10 +72,9 @@ describe('SubstrateCoin MPCv2 recovery helpers:', function () { | |
| }); | ||
|
|
||
| describe('addSubstrateRecoverySignature()', function () { | ||
| // EDDSAUtils.* are exported via `export * as Namespace`, compiling to non-configurable | ||
| // property getters — sinon cannot replace them. Instead, SubstrateCoin exposes | ||
| // getEddsaMpcV2RecoveryKeyShares() and signEddsaMpcV2Recovery() as protected methods | ||
| // so they can be stubbed on the instance (own property shadows the prototype). | ||
| // signEddsaMpcV2RecoveryTx is a directly-imported module binding — sinon cannot intercept | ||
| // it after import. SubstrateCoin exposes signSubstrateMpcV2Recovery() as a protected | ||
| // wrapper so tests can stub it on the instance (own property shadows the prototype). | ||
| // EDDSAMethods.getTSSSignature is a regular writable property — sinon can stub it directly. | ||
| let addSignatureStub: sinon.SinonStub; | ||
| let coin: SubstrateCoinTestAccessor; | ||
|
|
@@ -89,12 +88,7 @@ describe('SubstrateCoin MPCv2 recovery helpers:', function () { | |
|
|
||
| it('should prepend ED25519 0x00 discriminant on MPCv2 path', async function () { | ||
| const rawSig = Buffer.alloc(64, 0xab); | ||
| sinon.stub(coin as unknown, 'getEddsaMpcV2RecoveryKeyShares').resolves({ | ||
| userKeyShare: 'ks1', | ||
| backupKeyShare: 'ks2', | ||
| commonKeyChain: MOCK_BITGO_KEY, | ||
| }); | ||
| sinon.stub(coin as unknown, 'signEddsaMpcV2Recovery').resolves(rawSig); | ||
| sinon.stub(coin as unknown, 'signSubstrateMpcV2Recovery').resolves(rawSig); | ||
|
|
||
| await coin.addSubstrateRecoverySignature( | ||
| { addSignature: addSignatureStub }, | ||
|
|
@@ -114,11 +108,9 @@ describe('SubstrateCoin MPCv2 recovery helpers:', function () { | |
| }); | ||
|
|
||
| it('should throw when commonKeyChain does not match bitgoKey on MPCv2 path', async function () { | ||
| sinon.stub(coin as unknown, 'getEddsaMpcV2RecoveryKeyShares').resolves({ | ||
| userKeyShare: 'ks1', | ||
| backupKeyShare: 'ks2', | ||
| commonKeyChain: 'mismatch', | ||
| }); | ||
| sinon | ||
| .stub(coin as unknown, 'signSubstrateMpcV2Recovery') | ||
| .rejects(new Error('EdDSA MPCv2 recovery: commonKeyChain from keycard does not match bitgoKey')); | ||
|
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. This test used to exercise the real Suggest either:
|
||
|
|
||
| await coin | ||
| .addSubstrateRecoverySignature( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,7 +59,8 @@ import { | |
| DeriveAddressOptions, | ||
| DeriveAddressResult, | ||
| UnexpectedAddressError, | ||
| EDDSAUtils, | ||
| isMpcV2Keycard, | ||
| signEddsaMpcV2RecoveryTx, | ||
| } from '@bitgo/sdk-core'; | ||
| import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc'; | ||
| import { BaseNetwork, CoinFamily, coins, SolCoin, BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; | ||
|
|
@@ -1700,7 +1701,7 @@ export class Sol extends BaseCoin { | |
| const userKey = params.userKey?.replace(/\s/g, '') ?? ''; | ||
|
|
||
| const isMpcV2 = params.walletPassphrase | ||
| ? !(await EDDSAUtils.isEddsaMpcV1SigningMaterial(userKey, params.walletPassphrase, this.bitgo)) | ||
| ? (await isMpcV2Keycard(userKey, params.walletPassphrase, this.bitgo)).version === 'v2' | ||
|
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. This used to be a cheap boolean check via Same pattern at the consolidations call site (~1823) and in For boolean-only detection, prefer keeping |
||
| : false; | ||
|
|
||
| const index = params.index || 0; | ||
|
|
@@ -1819,7 +1820,7 @@ export class Sol extends BaseCoin { | |
| // Detect once at the top to avoid decrypting the keycard on every iteration of the scan loop. | ||
| // For unsigned sweep (no passphrase), isMpcV2 is false — cold MPCv2 is out of scope. | ||
| const isMpcV2 = params.walletPassphrase | ||
| ? !(await EDDSAUtils.isEddsaMpcV1SigningMaterial(userKey, params.walletPassphrase, this.bitgo)) | ||
| ? (await isMpcV2Keycard(userKey, params.walletPassphrase, this.bitgo)).version === 'v2' | ||
| : false; | ||
|
|
||
| const baseAddressIndex = 0; | ||
|
|
@@ -1963,25 +1964,15 @@ export class Sol extends BaseCoin { | |
| ); | ||
| txBuilder.addSignature({ pub: bs58EncodedPublicKey } as PublicKey, signatureHex); | ||
| } else { | ||
| const { userKeyShare, backupKeyShare, commonKeyChain } = | ||
| await EDDSAUtils.getEddsaMpcV2RecoveryKeySharesFromReducedKey( | ||
| userKey, | ||
| backupKey, | ||
| params.walletPassphrase!, | ||
| this.bitgo | ||
| ); | ||
|
|
||
| if (commonKeyChain.toLowerCase() !== bitgoKey.toLowerCase()) { | ||
| throw new Error('EdDSA MPCv2 recovery: commonKeyChain from keycard does not match bitgoKey'); | ||
| } | ||
|
|
||
| const signature = await EDDSAUtils.signRecoveryEddsaMPCv2( | ||
| unsignedTransaction.signablePayload, | ||
| currPath, | ||
| userKeyShare, | ||
| backupKeyShare, | ||
| commonKeyChain | ||
| ); | ||
| const signature = await signEddsaMpcV2RecoveryTx({ | ||
| message: unsignedTransaction.signablePayload, | ||
| userKey, | ||
| backupKey, | ||
| walletPassphrase: params.walletPassphrase!, | ||
| bitgoKey, | ||
| derivationPath: currPath, | ||
| bitgo: this.bitgo, | ||
| }); | ||
| txBuilder.addSignature({ pub: bs58EncodedPublicKey } as PublicKey, signature); | ||
| } | ||
| } | ||
|
|
@@ -1991,29 +1982,11 @@ export class Sol extends BaseCoin { | |
| backupKey?: string, | ||
| walletPassphrase?: string | ||
| ): Promise<boolean> { | ||
| let isMpcV2 = false; | ||
| if (walletPassphrase) { | ||
| if (!userKey) { | ||
| throw new Error('missing userKey'); | ||
| } | ||
| if (!backupKey) { | ||
| throw new Error('missing backupKey'); | ||
| } | ||
| // Detect MPCv2 keycards — will throw if decryption fails (e.g., wrong password). | ||
| // MPCv1 keycards decrypt to JSON with uShare/bitgoYShare; MPCv2 keycards are CBOR. | ||
| try { | ||
| const isV1 = await EDDSAUtils.isEddsaMpcV1SigningMaterial( | ||
| userKey.replace(/\s/g, ''), | ||
| walletPassphrase, | ||
| this.bitgo | ||
| ); | ||
| isMpcV2 = !isV1; | ||
| } catch (e) { | ||
| // Re-wrap decryption errors with context | ||
| throw new Error(`Error decrypting user keychain: ${e instanceof Error ? e.message : String(e)}`); | ||
| } | ||
| } | ||
| return isMpcV2; | ||
| if (!walletPassphrase) return false; | ||
| if (!userKey) throw new Error('missing userKey'); | ||
| if (!backupKey) throw new Error('missing backupKey'); | ||
| const material = await isMpcV2Keycard(userKey.replace(/\s/g, ''), walletPassphrase, this.bitgo); | ||
|
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. Same double-decrypt concern here: this method only needs a boolean, but If we keep the material-returning helper, maybe add a dedicated boolean helper (or have this call |
||
| return material.version === 'v2'; | ||
| } | ||
|
|
||
| async broadcastTransaction({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,9 @@ import { | |
| BitGoBase, | ||
| decryptKeychainPrivateKey, | ||
| EDDSAMethods, | ||
| EDDSAUtils, | ||
| isMpcV2Keycard as sharedIsMpcV2Keycard, | ||
| signEddsaMpcV2RecoveryTx, | ||
| EddsaSigningMaterial, | ||
| InvalidAddressError, | ||
| KeyPair, | ||
| MPCAlgorithm, | ||
|
|
@@ -46,8 +48,6 @@ export interface TonParseTransactionOptions extends ParseTransactionOptions { | |
| toAddressBounceable?: boolean; | ||
| } | ||
|
|
||
| type TonSigningMaterial = { version: 'v1'; userPrv: string } | { version: 'v2'; encryptedUserKey: string }; | ||
|
|
||
| export class Ton extends BaseCoin { | ||
| protected readonly _staticsCoin: Readonly<StaticsBaseCoin>; | ||
| protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) { | ||
|
|
@@ -323,19 +323,8 @@ export class Ton extends BaseCoin { | |
| * Discriminated union carrying keycard version and decrypted V1 user key (to avoid re-decryption). | ||
| * V1 keycards are JSON; V2 keycards are CBOR-encoded reduced key shares. | ||
| */ | ||
| private async isMpcV2Keycard(userKey: string, walletPassphrase: string): Promise<TonSigningMaterial> { | ||
| const normalized = userKey.replace(/\s/g, ''); | ||
| let isV1: boolean; | ||
| try { | ||
| isV1 = await EDDSAUtils.isEddsaMpcV1SigningMaterial(normalized, walletPassphrase, this.bitgo); | ||
| } catch (e) { | ||
| throw new Error(`Error decrypting user keychain: ${e instanceof Error ? e.message : String(e)}`); | ||
| } | ||
| if (isV1) { | ||
| const userPrv = await this.decryptKeychain(normalized, walletPassphrase, 'user'); | ||
| return { version: 'v1', userPrv }; | ||
| } | ||
| return { version: 'v2', encryptedUserKey: normalized }; | ||
| private async isMpcV2Keycard(userKey: string, walletPassphrase: string): Promise<EddsaSigningMaterial> { | ||
|
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. Nit: this private wrapper is now a pure pass-through to |
||
| return sharedIsMpcV2Keycard(userKey, walletPassphrase, this.bitgo); | ||
| } | ||
|
|
||
| private async decryptKeychain(encryptedKey: string, passphrase: string, label: string): Promise<string> { | ||
|
|
@@ -347,7 +336,7 @@ export class Ton extends BaseCoin { | |
| } | ||
|
|
||
| private async addRecoverySignature( | ||
| signingMaterial: TonSigningMaterial, | ||
| signingMaterial: EddsaSigningMaterial, | ||
| txBuilder: TransactionBuilder, | ||
| senderAddr: string, | ||
| unsignedTransaction: any, | ||
|
|
@@ -357,23 +346,15 @@ export class Ton extends BaseCoin { | |
| walletPassphrase: string | ||
| ): Promise<void> { | ||
| if (signingMaterial.version === 'v2') { | ||
| const { userKeyShare, backupKeyShare, commonKeyChain } = | ||
| await EDDSAUtils.getEddsaMpcV2RecoveryKeySharesFromReducedKey( | ||
| signingMaterial.encryptedUserKey, | ||
| backupKey, | ||
| walletPassphrase, | ||
| this.bitgo | ||
| ); | ||
| if (commonKeyChain.toLowerCase() !== bitgoKey.toLowerCase()) { | ||
| throw new Error('EdDSA MPCv2 recovery: commonKeyChain from keycard does not match bitgoKey'); | ||
| } | ||
| const signature = await EDDSAUtils.signRecoveryEddsaMPCv2( | ||
| unsignedTransaction.signablePayload, | ||
| currPath, | ||
| userKeyShare, | ||
| backupKeyShare, | ||
| commonKeyChain | ||
| ); | ||
| const signature = await signEddsaMpcV2RecoveryTx({ | ||
| message: unsignedTransaction.signablePayload, | ||
| userKey: signingMaterial.encryptedUserKey, | ||
| backupKey, | ||
| walletPassphrase, | ||
| bitgoKey, | ||
| derivationPath: currPath, | ||
| bitgo: this.bitgo, | ||
| }); | ||
| txBuilder.addSignature({ pub: senderAddr } as PublicKey, signature); | ||
| } else { | ||
| const userSigningMaterial = JSON.parse(signingMaterial.userPrv) as EDDSAMethodTypes.UserSigningMaterial; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.