feat(sdk-core): extract isMpcV2Keycard and signEddsaMpcV2RecoveryTx into shared utils - #9449
feat(sdk-core): extract isMpcV2Keycard and signEddsaMpcV2RecoveryTx into shared utils#9449vibhavgo wants to merge 3 commits into
Conversation
1340716 to
32bab87
Compare
32bab87 to
37ba5b1
Compare
cba5249 to
541f899
Compare
Marzooqa
left a comment
There was a problem hiding this comment.
Nice consolidation — moving isMpcV2Keycard/signEddsaMpcV2RecoveryTx into sdk-core is the right call since the logic is genuinely duplicated across substrate/sol/ton (and soon ada/sui/near/iota per WCI-1276).
Two follow-up cleanup spots in abstractSubstrateCoin.ts now that the shared helpers exist — left inline.
|
Will defer to marzooqa review |
davidkaplanbitgo
left a comment
There was a problem hiding this comment.
unclear why a BTC review is needed. Don't have enough context
8ce5176 to
7c1c3cc
Compare
Ticket: WCI-1276 Add EddsaSigningMaterial discriminated union, isMpcV2Keycard, and signEddsaMpcV2RecoveryTx to eddsaMPCv2.ts. Replace duplicated inline implementations in abstract-substrate and sdk-coin-sol with these shared helpers. sjcl fallback retained for optional bitgo parameter. Export all new symbols from sdk-core package root. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ticket: WCI-1276 Replace inline isMpcV2Keycard and addRecoverySignature MPCv2 block in sdk-coin-ton with shared helpers from sdk-core. Removes TonSigningMaterial local type in favour of EddsaSigningMaterial. Drop-in — no behaviour change, no test modifications. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Keychain Ticket: WCI-1276 Address PR review comments: - Remove decryptKeychain private method; inline decryptKeychainPrivateKey at its one remaining call site (MPCv1 backup-key path) - Replace getEddsaMpcV2RecoveryKeyShares + signEddsaMpcV2Recovery split wrappers with a single signSubstrateMpcV2Recovery wrapper that delegates to signEddsaMpcV2RecoveryTx from sdk-core - Remove EDDSAUtils import (no longer referenced) - Update test stubs to target the new consolidated wrapper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7c1c3cc to
070b63f
Compare
| * For v2: returns the encrypted key as-is for use with signEddsaMpcV2RecoveryTx. | ||
| * Identical logic across all EdDSA coin recovery implementations. | ||
| */ | ||
| export async function isMpcV2Keycard( |
There was a problem hiding this comment.
Naming nit: isMpcV2Keycard reads like a boolean predicate, but this returns EddsaSigningMaterial. That mismatch encourages the Sol boolean-only misuse below.
Consider renaming to something like getEddsaSigningMaterial / detectEddsaKeycardMaterial before more coins adopt it under WCI-1276.
| } | ||
| if (isV1) { | ||
| if (!bitgo) throw new Error('bitgo instance required for MPCv1 keycard decryption'); | ||
| const userPrv = await bitgo.decrypt({ input: normalized, password: walletPassphrase }); |
There was a problem hiding this comment.
For v1 keycards this decrypts twice: once inside isEddsaMpcV1SigningMaterial, then again here via bitgo.decrypt.
Since isEddsaMpcV1SigningMaterial already materializes the plaintext to JSON-parse it, can we reuse that decrypted value (or fold detection into one decrypt) so callers don't pay a second Argon2id/SJCL decrypt?
Also: isEddsaMpcV1SigningMaterial still supports sjcl fallback when bitgo is omitted, but this path then hard-requires bitgo for v1. Worth clarifying in the docstring that detection can be sjcl-only, while materializing v1 plaintext requires bitgo.
|
|
||
| const isMpcV2 = params.walletPassphrase | ||
| ? !(await EDDSAUtils.isEddsaMpcV1SigningMaterial(userKey, params.walletPassphrase, this.bitgo)) | ||
| ? (await isMpcV2Keycard(userKey, params.walletPassphrase, this.bitgo)).version === 'v2' |
There was a problem hiding this comment.
This used to be a cheap boolean check via isEddsaMpcV1SigningMaterial. Switching to isMpcV2Keycard(...).version === 'v2' means v1 keycards decrypt twice and then discard userPrv.
Same pattern at the consolidations call site (~1823) and in isMpcv2SigningMaterial (~1988).
For boolean-only detection, prefer keeping isEddsaMpcV1SigningMaterial (or a thin boolean helper) until the shared helper stops double-decrypting on v1.
| 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); |
There was a problem hiding this comment.
Same double-decrypt concern here: this method only needs a boolean, but isMpcV2Keycard materializes full v1 signing material just to compare .version === 'v2'.
If we keep the material-returning helper, maybe add a dedicated boolean helper (or have this call isEddsaMpcV1SigningMaterial directly) so recovery/consolidation hot paths don't pay the extra decrypt.
| }); | ||
| sinon | ||
| .stub(coin as unknown, 'signSubstrateMpcV2Recovery') | ||
| .rejects(new Error('EdDSA MPCv2 recovery: commonKeyChain from keycard does not match bitgoKey')); |
There was a problem hiding this comment.
This test used to exercise the real commonKeyChain mismatch check. After the consolidation it stubs signSubstrateMpcV2Recovery to reject with that exact error, then asserts the same message — so it's now tautological and no longer validates substrate wiring.
Suggest either:
- Drop this case (sdk-core already covers mismatch in
signEddsaMpcV2RecoveryTx), or - Keep a thin propagation/args assertion without hardcoding the failure inside the stub.
| const result = await isMpcV2Keycard(encrypted, PASSPHRASE, mockBitgo); | ||
|
|
||
| assert.strictEqual(result.version, 'v1'); | ||
| assert.ok('userPrv' in result); |
There was a problem hiding this comment.
Minor: this only asserts version === 'v1' and that userPrv exists. Please also assert the decrypted content, e.g. result.userPrv === JSON.stringify(MPCv1_MATERIAL) (and/or that bitgo.decrypt was called with the normalized key).
| return { version: 'v1', userPrv }; | ||
| } | ||
| return { version: 'v2', encryptedUserKey: normalized }; | ||
| private async isMpcV2Keycard(userKey: string, walletPassphrase: string): Promise<EddsaSigningMaterial> { |
There was a problem hiding this comment.
Nit: this private wrapper is now a pure pass-through to sharedIsMpcV2Keycard. Fine if you want a stable local call site, but if nothing stubs it, callers could use the shared helper directly and drop the alias import.
Summary
EddsaSigningMaterialdiscriminated union (v1 | v2) toeddsaMPCv2.tsisMpcV2Keycard(userKey, walletPassphrase, bitgo?)— detects v1 (JSON/SJCL) vs v2 (CBOR) keycard, returns typed signing material; sjcl fallback retained for optionalbitgosignEddsaMpcV2RecoveryTx(params)— full MPCv2 recovery signing: decrypt key shares → validatecommonKeyChain→ MPS DSGsdk-corepackage rootabstract-substrateandsdk-coin-solwith the shared helpers — drop-in, no behaviour change, no test modificationsContext
WCI-1276 —
isMpcV2KeycardandsignEddsaMpcV2RecoveryTxwere duplicated inline across coin modules (Substrate/DOT/POLYX, SOL). This PR extracts them intosdk-coreand replaces all call sites.This branch is based on
feat/abstract-substrate/WCI-1239and should be merged after that branch lands.Modules changed
@bitgo/sdk-core@bitgo/abstract-substrateisMpcV2Keycardto shared helper; removes localSubstrateSigningMaterialtype@bitgo/sdk-coin-solisMpcv2SigningMaterialand MPCv2 signing block to shared helpersTest plan
yarn testinmodules/sdk-core— 18 passingyarn buildinmodules/abstract-substrate— cleanyarn testinmodules/sdk-coin-sol— 482 passing (3 pre-existing env-var failures unrelated to this PR)🤖 Generated with Claude Code