Skip to content

feat(sdk-coin-ada): add MPCv2 support to recoverConsolidations - #9497

Open
ralph-bitgo[bot] wants to merge 2 commits into
masterfrom
feat/sdk-coin-ada/WCI-1232-mpcv2-recover-consolidations
Open

feat(sdk-coin-ada): add MPCv2 support to recoverConsolidations#9497
ralph-bitgo[bot] wants to merge 2 commits into
masterfrom
feat/sdk-coin-ada/WCI-1232-mpcv2-recover-consolidations

Conversation

@ralph-bitgo

@ralph-bitgo ralph-bitgo Bot commented Aug 13, 2026

Copy link
Copy Markdown

What

  • Adds an optional precomputedMaterial?: EddsaSigningMaterial parameter to Ada.recover(). When supplied, recover() skips its own getEddsaSigningMaterial call and dispatches straight to MPCv1 (EDDSAMethods.getTSSSignature) or MPCv2 (signEddsaMpcV2RecoveryTx) signing based on signingMaterial.version.
  • In Ada.recoverConsolidations(), detects signing material once before the scan loop (guarded by assert(params.userKey, 'missing userKey') when walletPassphrase is set) and threads it into every recover() call via the new parameter, instead of letting each call re-detect it.
  • Reuses the shared getEddsaSigningMaterial / signEddsaMpcV2RecoveryTx / decryptKeychainPrivateKey helpers from @bitgo/sdk-core (already merged in WCI-1276), matching the pattern established for DOT (WCI-1236) and SOL (WCI-496).
  • Adds unit tests: MPCv2 signed recovery/consolidation routing, getEddsaSigningMaterial called exactly once across a multi-index consolidation scan, MPCv1 regression (signing unchanged), unsigned/cold-path regression (no passphrase), the new missing-userKey fail-fast guard, and the backup-keycard decrypt-failure error path.

Why

recoverConsolidations() scans up to 20+ receive-address indexes and calls recover() once per index. For MPCv2 wallets, recover() previously ran keycard detection (getEddsaSigningMaterial) on every call, decrypting the same keycard N times unnecessarily across a scan range. Wallets are migrating EdDSA MPC from the Zengo implementation (MPCv1) to Silence Labs (MPCv2), so this closes the gap for ADA's consolidation recovery hot path, depending on Ada.recover()'s own MPCv2 support (WCI-1222).

Test plan

  • nix develop . --command yarn tsc --build --force . in modules/sdk-coin-ada — compiles cleanly
  • nix develop . --command yarn eslint --quiet src/ada.ts test/unit/ada.ts — no errors
  • BITGOJS_TEST_PASSWORD=... yarn unit-test in modules/sdk-coin-ada — 176 passing, 0 failing (includes 9 new MPCv2 tests)

Ticket: WCI-1232

Add MPCv2 signed recovery to Ada.recover() via an optional
precomputedMaterial parameter, and hoist signing-material detection
in Ada.recoverConsolidations() to run once before the scan loop
instead of once per recover() call, guarding on userKey the same way
recover() already does. Reuses the shared getEddsaSigningMaterial /
signEddsaMpcV2RecoveryTx helpers from @bitgo/sdk-core, matching the
pattern already established for DOT (WCI-1236) and SOL (WCI-496).

Wallets are migrating EdDSA MPC from Zengo (MPCv1) to Silence Labs
(MPCv2). recoverConsolidations() scans up to 20+ receive-address
indexes and previously called recover() once per index, which for
MPCv2 wallets would decrypt the keycard N times unnecessarily since
detection happened inside recover() on every call.

Ticket: WCI-1232
Session-Id: 31bb6e8e-db75-4405-8c2b-bd05532cd95f
Task-Id: e959f25c-1bf7-4294-a67f-904be99c4afe
@linear-code

linear-code Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

WCI-1232

@bitgo-ai-agent-dev
bitgo-ai-agent-dev Bot force-pushed the feat/sdk-coin-ada/WCI-1232-mpcv2-recover-consolidations branch from 6b12004 to e5a1b98 Compare August 13, 2026 12:56
@ralph-bitgo
ralph-bitgo Bot force-pushed the feat/sdk-coin-ada/WCI-1232-mpcv2-recover-consolidations branch from e5a1b98 to fd858c9 Compare August 13, 2026 12:56
@vibhavgo
vibhavgo marked this pull request as ready for review August 13, 2026 14:44
@vibhavgo
vibhavgo requested a review from a team as a code owner August 13, 2026 14:44
@vibhavgo

Copy link
Copy Markdown
Contributor

AI-generated review — auto-generated by Claude Code (wallet-platform:review-pr). Please use your own judgment before acting on any findings.


Phase 1 — Summary

Adds optional precomputedMaterial?: EddsaSigningMaterial to Ada.recover(), enabling MPCv2 vs MPCv1 dispatch based on signingMaterial.version. Ada.recoverConsolidations() now detects signing material once before the scan loop and threads it into each recover() call via precomputedMaterial, eliminating repeated keycard decryption across up to 20+ index iterations. Reuses getEddsaSigningMaterial / signEddsaMpcV2RecoveryTx / decryptKeychainPrivateKey helpers from sdk-core, matching the DOT (WCI-1236) and SOL (WCI-496) pattern.

Files changed: 2 — modules/sdk-coin-ada/src/ada.ts, modules/sdk-coin-ada/test/unit/ada.ts | +329 / -35 lines

flowchart TD
    A["recover(params, precomputedMaterial?)"] --> B{precomputedMaterial\nprovided?}
    B -- yes --> C[skip getEddsaSigningMaterial]
    B -- no --> D["getEddsaSigningMaterial(userKey, passphrase)"]
    C --> E{signingMaterial.version}
    D --> E
    E -- v2 --> F["signAdaMpcV2Recovery → signEddsaMpcV2RecoveryTx"]
    E -- v1 --> G["decryptKeychainPrivateKey → EDDSAMethods.getTSSSignature"]
    F --> H[txBuilder.addSignature]
    G --> H

    RC["recoverConsolidations()"] --> RP{walletPassphrase\nset?}
    RP -- yes --> RA["assert(params.userKey)\ngetEddsaSigningMaterial ONCE"]
    RP -- no --> RB[signingMaterial = undefined]
    RA --> RL["for i in startIdx..endIdx"]
    RB --> RL
    RL --> RR["recover(recoverParams, signingMaterial)"]
Loading

Phase 2 — Ticket Alignment

Ticket: WCI-1232

PR title: feat(sdk-coin-ada): add MPCv2 support to recoverConsolidations

The implementation matches the ticket scope. The PR also delivers MPCv2 support to recover() itself (WCI-1222 dependency), which is a prerequisite and correctly called out in the PR body. No misalignment detected.


Phase 3 — Index Analysis

No new database queries introduced.


Phase 4 — Type Safety

No as any, @ts-ignore, or double casts in added lines.

Sinon spy cast uses the safe as unknown as { getEddsaSigningMaterial: unknown } pattern — correct.

One pre-existing (not introduced) any[] on consolidationTransactions is unchanged — not a regression.


Phase 5 — MPCv2 Correctness & Test Quality

Correctness

  • getEddsaSigningMaterial called with whitespace-stripped userKey — recover() strips explicitly (params.userKey.replace(/\s/g, '')); recoverConsolidations() passes raw but the wrapper getEddsaSigningMaterial() calls sharedGetEddsaSigningMaterial(userKey.replace(/\s/g, ''), ...) internally.
  • assert() guards in recover() for userKey/backupKey/walletPassphrase — recoverConsolidations has assert(params.userKey, 'missing userKey') before the loop, but inside recover() itself the added code does params.userKey.replace(/\s/g, '') without an assert guard. If params.userKey is undefined, the error thrown will be a generic TypeError: Cannot read properties of undefined (reading 'replace') rather than a clear message. The guard pattern used in recoverConsolidations should be mirrored in recover().
  • ✅ Unsigned sweep path untouched — the else branch (no walletPassphrase) is unmodified.
  • decryptKeychainPrivateKey used for MPCv1 backup key — correct helper from sdk-core.
  • recoverConsolidations(): signing material detected ONCE before the loop, passed as precomputedMaterial to each recover() call.
  • recover(): correctly short-circuits with precomputedMaterial ?? (await this.getEddsaSigningMaterial(...)).

Test Quality

  • ✅ v2 path: asserts getTSSSignature NOT called (sandBox.assert.notCalled(getTSSSignatureSpy)).
  • ✅ v1 path: asserts getTSSSignature WAS called (sandBox.assert.calledOnce(getTSSSignatureSpy)).
  • ❌ No crypto verification using nacl.sign.detached.verify — the MPCv2 recovery test validates res.hasOwnProperty('serializedTx') and parses the output address, but never verifies the Ed25519 signature cryptographically. The DOT canonical pattern includes nacl.sign.detached.verify to confirm the produced signature is actually valid.
  • ✅ Mismatched bitgoKey vs commonKeyChain error test present — rejectedWith('EdDSA MPCv2 recovery: commonKeyChain from keycard does not match bitgoKey').
  • ✅ Consolidations: getEddsaSigningMaterial asserted called exactly once across 2-funded-index scan.
  • ❌ Guard tests incomplete — test for missing userKey in recoverConsolidations is present, but no test for the case where backupKey is missing in recover() directly, and no test for missing walletPassphrase guard behavior in recover().

Consistency vs DOT (#9484)

  • ✅ Sinon stubs use as unknown as { method: unknown } (not as any) — correct.
  • ✅ Describe block naming ('Recover Transactions (MPCv2):', 'Build Consolidation Recoveries (MPCv2):') is consistent with the DOT pattern style.

Summary of ❌ Findings

# Finding Severity
1 recover() lacks assert() guards for params.userKey / params.backupKey before .replace() calls — unhelpful TypeError if undefined Low
2 No nacl.sign.detached.verify crypto check in MPCv2 recovery test — signature validity not confirmed end-to-end Low
3 Missing guard tests for backupKey and walletPassphrase absence in recover() directly Low

None of these are blockers, but items 1 and 2 are the most likely to cause confusion in future debugging.


Generated by [wallet-platform:review-pr] via Claude Code

Add two regression tests: recoverConsolidations() now throws
'missing userKey' before scanning any address when walletPassphrase
is set but userKey is omitted, and recover() throws a clear error
when the MPCv1 backup keycard fails to decrypt.

Automated review after the MPCv2 recoverConsolidations commit flagged
both branches as new but untested, risking a silent regression if a
future refactor moved the fail-fast guard or broke the
decryptKeychainPrivateKey error path.

Ticket: WCI-1232
Session-Id: 31bb6e8e-db75-4405-8c2b-bd05532cd95f
Task-Id: e959f25c-1bf7-4294-a67f-904be99c4afe
@vibhavgo
vibhavgo force-pushed the feat/sdk-coin-ada/WCI-1232-mpcv2-recover-consolidations branch from fd858c9 to 9b72678 Compare August 14, 2026 07:11
@vibhavgo
vibhavgo requested a review from Marzooqa August 14, 2026 07:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant