Skip to content

fix(sdk-coin-ada,sdk-coin-iota): validate precomputed EdDSA signing material - #9580

Merged
Marzooqa merged 1 commit into
masterfrom
marzooqakather498/wci-1460-test-iota-dkg-dsg-and-recovery-flows-on-staging
Aug 28, 2026
Merged

Marzooqa merged 1 commit into
masterfrom
marzooqakather498/wci-1460-test-iota-dkg-dsg-and-recovery-flows-on-staging

Conversation

@Marzooqa

Copy link
Copy Markdown
Contributor

Summary

Found while manually testing ADA/NEAR/TON/IOTA non-BitGo recovery end-to-end through the Wallet Recovery Wizard against staging (see WCI-1460).

Wallet Recovery Wizard's generic Electron recover IPC handler always passes a second positional argument (openSSLBytes, an ArrayBuffer meant only for EVM-like coins) to every coin's recover() call:

return await baseCoin.recover({ ...parameters, openSSLBytes }, openSSLBytes);

Ada.recover() and Iota.recover() both accept an optional second parameter of their own — precomputedMaterial, used by recoverConsolidations() to avoid re-decrypting the keycard once per scanned index. WRW's stray openSSLBytes argument gets silently misinterpreted as that signing material:

const signingMaterial = precomputedMaterial ?? (await this.getEddsaSigningMaterial(...));
// signingMaterial = the ArrayBuffer, since it's truthy
JSON.parse(signingMaterial.userPrv)  // ArrayBuffer has no .userPrv -> JSON.parse(undefined)
// -> SyntaxError: "undefined" is not valid JSON

sdk-coin-dot already guards against exactly this with a local isEddsaSigningMaterial() type guard (added when DOT's recoverConsolidations was written) — which is why DOT was unaffected. This PR extracts that guard into sdk-core (next to EddsaSigningMaterial/getEddsaSigningMaterial/signEddsaMpcV2RecoveryTx) and applies it in ADA and IOTA's signRecoveryTransaction().

sdk-coin-sui has the same latent issue but is intentionally left untouched in this PR (out of scope — no SUI changes requested).

Changes

  • sdk-core: add and export isEddsaSigningMaterial() type guard
  • sdk-coin-ada: validate precomputedMaterial shape before trusting it in signRecoveryTransaction()
  • sdk-coin-iota: same

Test plan

  • Manually verified ADA and IOTA non-BitGo recovery end-to-end via Wallet Recovery Wizard against staging (broadcast + confirmed on-chain) — reproduced the bug pre-fix, confirmed resolved post-fix
  • yarn unit-test for sdk-coin-ada (183 passing) and sdk-coin-iota (255 passing)
  • tsc --build and eslint clean for all touched packages

TICKET: WCI-1460

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

WCI-1460

…aterial

Discovered while manually verifying ADA/IOTA non-BitGo recovery against
Wallet Recovery Wizard on staging: WRW's generic Electron 'recover' IPC
handler always passes a second positional argument (openSSLBytes, meant
only for EVM-like coins) to every coin's recover() call. Ada.recover()
and Iota.recover() accept an optional second positional parameter of
their own (precomputedMaterial, used by recoverConsolidations() to avoid
re-decrypting the keycard per scanned index), so that ArrayBuffer was
silently misinterpreted as signing material, causing:

  JSON.parse(signingMaterial.userPrv) -> JSON.parse(undefined)
  -> SyntaxError: "undefined" is not valid JSON

Add a shared isEddsaSigningMaterial() type guard to sdk-core (next to
EddsaSigningMaterial/getEddsaSigningMaterial/signEddsaMpcV2RecoveryTx)
and use it in Ada/Iota's signRecoveryTransaction() before trusting a
caller-supplied precomputedMaterial, falling back to real detection
otherwise. This mirrors the guard sdk-coin-dot already had in place
when it added the same optimization, which is why DOT was unaffected.

Verified end-to-end: ADA and IOTA non-BitGo recovery via WRW against
staging now build and broadcast signed transactions successfully.

TICKET: WCI-1460
@Marzooqa
Marzooqa force-pushed the marzooqakather498/wci-1460-test-iota-dkg-dsg-and-recovery-flows-on-staging branch from 128a8b8 to 6d30f56 Compare August 27, 2026 10:23
@Marzooqa

Copy link
Copy Markdown
Contributor Author

@claude

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Marzooqa's task —— View job


Code Review Complete ✅

Todo List

  • Review core type guard implementation in sdk-core
  • Review ada.ts changes and integration
  • Review iota.ts changes and integration
  • Review test coverage and quality
  • Check for potential edge cases or improvements
  • Provide comprehensive feedback

🔍 Review Summary

This is an excellent, focused fix that resolves a real-world bug affecting ADA and IOTA wallet recovery. The solution is well-architected and thoroughly tested.

✅ Positive Aspects

1. Root Cause Analysis

  • Correctly identified the issue: Wallet Recovery Wizard's generic recover IPC handler passes openSSLBytes (ArrayBuffer) as second parameter to all coins
  • Clear understanding that this gets misinterpreted as precomputedMaterial in EdDSA coins

2. Smart Code Reuse

  • Extracted existing isEddsaSigningMaterial type guard from sdk-coin-dot into shared sdk-core (/home/runner/work/BitGoJS/BitGoJS/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts:1353-1367)
  • Proper export through sdk-core/src/index.ts:37
  • Applied consistently in both ADA and IOTA implementations

3. Robust Type Guard Implementation

The type guard validation is comprehensive:

export function isEddsaSigningMaterial(value: unknown): value is EddsaSigningMaterial {
  if (typeof value !== 'object' || value === null) return false;
  if (!('version' in value)) return false;
  if (value.version === 'v1') {
    return 'userPrv' in value && typeof value.userPrv === 'string';
  }
  if (value.version === 'v2') {
    return 'encryptedUserKey' in value && typeof value.encryptedUserKey === 'string';
  }
  return false;
}

4. Correct Integration in Coin Implementations

Both ADA and IOTA use the validation properly:

  • ADA (ada.ts:463-465): Validates before trusting precomputedMaterial
  • IOTA (iota.ts:861-863): Same pattern applied consistently

5. Excellent Test Coverage

  • Core tests: Comprehensive validation of type guard edge cases including the specific ArrayBuffer regression case
  • Integration tests: Both ADA and IOTA have specific tests for the WRW openSSLBytes collision scenario
  • Tests verify that malformed precomputedMaterial is ignored and detection falls back to normal flow

🔧 Technical Implementation Quality

Security Considerations ✅

  • No security vulnerabilities introduced
  • Defensive programming approach prevents runtime errors
  • Type safety maintained throughout

Performance Impact ✅

  • Minimal overhead: simple object shape validation
  • recoverConsolidations efficiency preserved through precomputedMaterial optimization
  • Fallback to normal detection when validation fails

Backward Compatibility ✅

  • No breaking changes to existing APIs
  • Graceful degradation when type guard fails
  • SUI intentionally left unchanged (out of scope)

📋 Minor Observations

  1. Code Style: Follows existing patterns and conventions consistently
  2. Documentation: Clear comments explaining the purpose and context
  3. Error Handling: Proper fallback behavior when validation fails
  4. Test Quality: Regression tests specifically target the identified issue

🎯 Verification

The fix addresses the exact error described in the PR:

JSON.parse(signingMaterial.userPrv)  // ArrayBuffer has no .userPrv -> JSON.parse(undefined)
// -> SyntaxError: "undefined" is not valid JSON

By validating the shape before trusting precomputedMaterial, this error is prevented.

✨ Recommendation

APPROVE - This is a high-quality fix that:

  • Solves the reported issue effectively
  • Uses proper software engineering practices (DRY principle, type safety)
  • Includes comprehensive test coverage
  • Maintains backward compatibility
  • Follows established codebase patterns

The implementation is minimal, focused, and exactly addresses the problem without over-engineering.


Job Run: View Details
Branch: marzooqakather498/wci-1460-test-iota-dkg-dsg-and-recovery-flows-on-staging

@Marzooqa
Marzooqa marked this pull request as ready for review August 27, 2026 12:36
@Marzooqa
Marzooqa requested review from a team as code owners August 27, 2026 12:36
@Marzooqa
Marzooqa merged commit e12631b into master Aug 28, 2026
25 checks passed
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.

4 participants