From 288d4d56c75eb4e5337ead506cb54ae9a3fb791d Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Tue, 21 Oct 2025 16:06:37 -0400 Subject: [PATCH 1/5] feat(sdk-core): add MPC EdDSA address verification TICKET: WP-6378 --- .../sdk-core/src/bitgo/baseCoin/baseCoin.ts | 3 +- .../sdk-core/src/bitgo/baseCoin/iBaseCoin.ts | 11 ++-- .../bitgo/utils/tss/addressVerification.ts | 50 +++++++++++++++++++ modules/sdk-core/src/bitgo/utils/tss/index.ts | 1 + 4 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts diff --git a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts index 45332efb46..14d615efb5 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts @@ -47,6 +47,7 @@ import { VerifyTransactionOptions, AuditKeyParams, AuditDecryptedKeyParams, + TssVerifyAddressOptions, } from './iBaseCoin'; import { IInscriptionBuilder } from '../inscriptionBuilder'; import { @@ -346,7 +347,7 @@ export abstract class BaseCoin implements IBaseCoin { * @param params * @return true iff address is a wallet address. Must return false if address is outside wallet. */ - abstract isWalletAddress(params: VerifyAddressOptions): Promise; + abstract isWalletAddress(params: VerifyAddressOptions | TssVerifyAddressOptions): Promise; /** * convert address into desired address format. diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index e22677c966..b0add6a9fd 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -149,15 +149,18 @@ export interface VerifyAddressOptions { addressType?: string; keychains?: { pub: string; - commonKeychain?: string; }[]; error?: string; coinSpecific?: AddressCoinSpecific; impliedForwarderVersion?: number; } -export interface TssVerifyAddressOptions extends VerifyAddressOptions { - chain: string; +export interface TssVerifyAddressOptions { + address: string; + keychains: { + commonKeychain: string; + }[]; + chain?: string; index: string; } @@ -552,7 +555,7 @@ export interface IBaseCoin { explainTransaction(options: Record): Promise | undefined>; verifyTransaction(params: VerifyTransactionOptions): Promise; verifyAddress(params: VerifyAddressOptions): Promise; - isWalletAddress(params: VerifyAddressOptions, wallet?: IWallet): Promise; + isWalletAddress(params: VerifyAddressOptions | TssVerifyAddressOptions, wallet?: IWallet): Promise; canonicalAddress(address: string, format: unknown): string; supportsBlockTarget(): boolean; supportsLightning(): boolean; diff --git a/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts b/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts new file mode 100644 index 0000000000..11362f59ab --- /dev/null +++ b/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts @@ -0,0 +1,50 @@ +import { InvalidAddressError, TssVerifyAddressOptions } from '../../baseCoin/iBaseCoin'; +import { EDDSAMethods } from '../../tss'; + +/** + * Verifies if an address belongs to a wallet using EdDSA TSS MPC derivation. + * This is a common implementation for EdDSA-based MPC coins (SOL, DOT, SUI, TON, IOTA, etc.) + * + * @param params - Verification options including keychains, address, and derivation index + * @param isValidAddress - Coin-specific function to validate address format + * @param getAddressFromPublicKey - Coin-specific function to convert public key to address + * @returns true if the address matches the derived address, false otherwise + * @throws {InvalidAddressError} if the address is invalid + * @throws {Error} if required parameters are missing or invalid + */ +export async function verifyEddsaTssWalletAddress( + params: TssVerifyAddressOptions, + isValidAddress: (address: string) => boolean, + getAddressFromPublicKey: (publicKey: string) => string +): Promise { + const { keychains, address, index } = params; + + if (!isValidAddress(address)) { + throw new InvalidAddressError(`invalid address: ${address}`); + } + + if (!keychains || keychains.length === 0) { + throw new Error('missing required param keychains'); + } + + // For MPC coins, commonKeychain should be the same for all keychains + const commonKeychain = keychains[0].commonKeychain as string; + if (!commonKeychain) { + throw new Error('missing required param commonKeychain'); + } + + // Verify all keychains have the same commonKeychain + for (const keychain of keychains) { + if (keychain.commonKeychain !== commonKeychain) { + throw new Error('all keychains must have the same commonKeychain for MPC coins'); + } + } + + // Only perform derivation once since commonKeychain is the same + const MPC = await EDDSAMethods.getInitializedMpcInstance(); + const derivationPath = 'm/' + index; + const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64); + const expectedAddress = getAddressFromPublicKey(derivedPublicKey); + + return address === expectedAddress; +} diff --git a/modules/sdk-core/src/bitgo/utils/tss/index.ts b/modules/sdk-core/src/bitgo/utils/tss/index.ts index 04b749a5af..2276906b6c 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/index.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/index.ts @@ -14,3 +14,4 @@ export { ITssUtils, IEddsaUtils, TxRequest, EddsaUnsignedTransaction } from './e export * as BaseTssUtils from './baseTSSUtils'; export * from './baseTypes'; +export * from './addressVerification'; From e42fb85505c1fe9d5fe7d18f227377e2d844af77 Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Tue, 21 Oct 2025 16:12:53 -0400 Subject: [PATCH 2/5] feat(sdk-coin-sol): implement isWalletAddress for address verfn TICKET: WP-6459 --- modules/sdk-coin-sol/src/sol.ts | 22 ++++++-- modules/sdk-coin-sol/test/unit/sol.ts | 80 +++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/modules/sdk-coin-sol/src/sol.ts b/modules/sdk-coin-sol/src/sol.ts index 9143ea90af..940ae2476b 100644 --- a/modules/sdk-coin-sol/src/sol.ts +++ b/modules/sdk-coin-sol/src/sol.ts @@ -23,7 +23,6 @@ import { ITokenEnablement, KeyPair, Memo, - MethodNotImplementedError, MPCAlgorithm, MPCConsolidationRecoveryOptions, MPCRecoveryOptions, @@ -50,8 +49,9 @@ import { TransactionExplanation, TransactionParams, TransactionRecipient, - VerifyAddressOptions, VerifyTransactionOptions, + TssVerifyAddressOptions, + verifyEddsaTssWalletAddress, } from '@bitgo/sdk-core'; import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc'; import { BaseNetwork, CoinFamily, coins, SolCoin, BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; @@ -560,8 +560,22 @@ export class Sol extends BaseCoin { return true; } - async isWalletAddress(params: VerifyAddressOptions): Promise { - throw new MethodNotImplementedError(); + async isWalletAddress(params: TssVerifyAddressOptions): Promise { + return verifyEddsaTssWalletAddress( + params, + (address) => this.isValidAddress(address), + (publicKey) => this.getAddressFromPublicKey(publicKey) + ); + } + + /** + * Converts a Solana public key to an address + * @param publicKey Hex-encoded public key (64 hex characters = 32 bytes) + * @returns Base58-encoded Solana address + */ + getAddressFromPublicKey(publicKey: string): string { + const publicKeyBuffer = Buffer.from(publicKey, 'hex'); + return base58.encode(publicKeyBuffer); } /** diff --git a/modules/sdk-coin-sol/test/unit/sol.ts b/modules/sdk-coin-sol/test/unit/sol.ts index 01b6a39062..f089793aa9 100644 --- a/modules/sdk-coin-sol/test/unit/sol.ts +++ b/modules/sdk-coin-sol/test/unit/sol.ts @@ -3353,4 +3353,84 @@ describe('SOL:', function () { ); }); }); + + describe('isWalletAddress', () => { + it('should verify valid wallet address with correct keychain and index', async function () { + const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY'; + const commonKeychain = + '8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd'; + const index = '1'; + const keychains = [{ commonKeychain }]; + + const result = await basecoin.isWalletAddress({ keychains, address, index }); + result.should.equal(true); + }); + + it('should return false for address with incorrect keychain', async function () { + const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY'; + const wrongKeychain = + '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + const index = '1'; + const keychains = [{ commonKeychain: wrongKeychain }]; + + const result = await basecoin.isWalletAddress({ keychains, address, index }); + result.should.equal(false); + }); + + it('should return false for address with incorrect index', async function () { + const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY'; + const commonKeychain = + '8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd'; + const wrongIndex = '999'; + const keychains = [{ commonKeychain }]; + + const result = await basecoin.isWalletAddress({ keychains, address, index: wrongIndex }); + result.should.equal(false); + }); + + it('should throw error for invalid address', async function () { + const invalidAddress = 'invalidaddress'; + const commonKeychain = + '8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd'; + const index = '1'; + const keychains = [{ commonKeychain }]; + + await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address: invalidAddress, index }), { + message: `invalid address: ${invalidAddress}`, + }); + }); + + it('should throw error when keychains are missing', async function () { + const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY'; + const index = '1'; + + await assert.rejects(async () => await basecoin.isWalletAddress({ address, index } as any), { + message: 'missing required param keychains', + }); + }); + + it('should throw error when keychains have different commonKeychains', async function () { + const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY'; + const commonKeychain1 = + '8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd'; + const commonKeychain2 = + '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + const index = '1'; + const keychains = [{ commonKeychain: commonKeychain1 }, { commonKeychain: commonKeychain2 }]; + + await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address, index }), { + message: 'all keychains must have the same commonKeychain for MPC coins', + }); + }); + }); + + describe('getAddressFromPublicKey', () => { + it('should convert public key to base58 address', function () { + const publicKey = '61220a9394802b1d1df37b35f7a3197970f48081092cee011fc98f7b71b2bd43'; + const expectedAddress = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY'; + + const address = basecoin.getAddressFromPublicKey(publicKey); + address.should.equal(expectedAddress); + }); + }); }); From 78026ba57b6e9cc6a3561e12558ec18c8f435155 Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Tue, 21 Oct 2025 16:15:51 -0400 Subject: [PATCH 3/5] feat(sdk-coin-dot): implement isWalletAddress for address verfn TICKET: WP-6460 --- modules/sdk-coin-dot/src/dot.ts | 12 ++-- modules/sdk-coin-dot/test/unit/dot.ts | 91 +++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/modules/sdk-coin-dot/src/dot.ts b/modules/sdk-coin-dot/src/dot.ts index 5bf5b26165..3066f3fa26 100644 --- a/modules/sdk-coin-dot/src/dot.ts +++ b/modules/sdk-coin-dot/src/dot.ts @@ -7,15 +7,14 @@ import { Environments, ExplanationResult, KeyPair, - MethodNotImplementedError, MPCAlgorithm, ParsedTransaction, ParseTransactionOptions, SignedTransaction, SignTransactionOptions as BaseSignTransactionOptions, UnsignedTransaction, - VerifyAddressOptions, VerifyTransactionOptions, + TssVerifyAddressOptions, EDDSAMethods, EDDSAMethodTypes, MPCTx, @@ -29,6 +28,7 @@ import { MultisigType, multisigTypes, AuditDecryptedKeyParams, + verifyEddsaTssWalletAddress, } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, coins, PolkadotSpecNameType } from '@bitgo/statics'; import { Interface, KeyPair as DotKeyPair, Transaction, TransactionBuilderFactory, Utils } from './lib'; @@ -642,8 +642,12 @@ export class Dot extends BaseCoin { return {}; } - async isWalletAddress(params: VerifyAddressOptions): Promise { - throw new MethodNotImplementedError(); + async isWalletAddress(params: TssVerifyAddressOptions): Promise { + return verifyEddsaTssWalletAddress( + params, + (address) => this.isValidAddress(address), + (publicKey) => this.getAddressFromPublicKey(publicKey) + ); } async verifyTransaction(params: VerifyTransactionOptions): Promise { diff --git a/modules/sdk-coin-dot/test/unit/dot.ts b/modules/sdk-coin-dot/test/unit/dot.ts index ee7f3c367b..64a22b20aa 100644 --- a/modules/sdk-coin-dot/test/unit/dot.ts +++ b/modules/sdk-coin-dot/test/unit/dot.ts @@ -2,6 +2,7 @@ import { BitGoAPI } from '@bitgo/sdk-api'; import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test'; import { randomBytes } from 'crypto'; import should = require('should'); +import assert = require('assert'); import { Dot, Tdot, KeyPair } from '../../src'; import * as testData from '../fixtures'; import { chainName, txVersion, genesisHash, specVersion } from '../resources'; @@ -670,4 +671,94 @@ describe('DOT:', function () { ); }); }); + + describe('isWalletAddress', () => { + it('should verify valid wallet address with correct keychain and index', async function () { + const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74'; + const commonKeychain = + '6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e'; + const index = '3'; + const keychains = [{ commonKeychain }]; + + const result = await basecoin.isWalletAddress({ keychains, address, index }); + result.should.equal(true); + }); + + it('should return false for address with incorrect keychain', async function () { + const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74'; + const wrongKeychain = + '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + const index = '3'; + const keychains = [{ commonKeychain: wrongKeychain }]; + + const result = await basecoin.isWalletAddress({ keychains, address, index }); + result.should.equal(false); + }); + + it('should return false for address with incorrect index', async function () { + const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74'; + const commonKeychain = + '6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e'; + const wrongIndex = '999'; + const keychains = [{ commonKeychain }]; + + const result = await basecoin.isWalletAddress({ keychains, address, index: wrongIndex }); + result.should.equal(false); + }); + + it('should throw error for invalid address', async function () { + const invalidAddress = 'invalidaddress'; + const commonKeychain = + '6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e'; + const index = '3'; + const keychains = [{ commonKeychain }]; + + await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address: invalidAddress, index }), { + message: `invalid address: ${invalidAddress}`, + }); + }); + + it('should throw error when keychains are missing', async function () { + const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74'; + const index = '3'; + + await assert.rejects(async () => await basecoin.isWalletAddress({ address, index } as any), { + message: 'missing required param keychains', + }); + }); + + it('should throw error when keychains have different commonKeychains', async function () { + const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74'; + const commonKeychain1 = + '6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e'; + const commonKeychain2 = + '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + const index = '3'; + const keychains = [{ commonKeychain: commonKeychain1 }, { commonKeychain: commonKeychain2 }]; + + await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address, index }), { + message: 'all keychains must have the same commonKeychain for MPC coins', + }); + }); + }); + + describe('getAddressFromPublicKey', () => { + it('should convert public key to SS58 address for testnet', function () { + const publicKey = '53845d7b6a6e4a666fa2a0f500b88849b02926da5590993731d2b428b7643690'; + const expectedAddress = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74'; + + const address = basecoin.getAddressFromPublicKey(publicKey); + address.should.equal(expectedAddress); + }); + + it('should convert public key to SS58 address for mainnet', function () { + const publicKey = '53845d7b6a6e4a666fa2a0f500b88849b02926da5590993731d2b428b7643690'; + // Mainnet uses different SS58 prefix (0) vs testnet (42) + const address = prodCoin.getAddressFromPublicKey(publicKey); + address.should.be.type('string'); + address.length.should.be.greaterThan(0); + // Should be different from testnet address + address.should.not.equal('5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74'); + }); + }); }); From e9982fe76248c14d171c4dd85bad60ae83086882 Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Tue, 21 Oct 2025 16:19:12 -0400 Subject: [PATCH 4/5] chore: re-use generic mpc address verfn helper TICKET: WP-6378 --- modules/sdk-coin-iota/src/iota.ts | 31 ++++--------------- modules/sdk-coin-sui/src/sui.ts | 13 ++++---- .../bitgo/utils/tss/addressVerification.ts | 3 +- 3 files changed, 14 insertions(+), 33 deletions(-) diff --git a/modules/sdk-coin-iota/src/iota.ts b/modules/sdk-coin-iota/src/iota.ts index d1a223acfe..5a0d4f4891 100644 --- a/modules/sdk-coin-iota/src/iota.ts +++ b/modules/sdk-coin-iota/src/iota.ts @@ -11,10 +11,9 @@ import { MultisigType, multisigTypes, MPCAlgorithm, - InvalidAddressError, - EDDSAMethods, TssVerifyAddressOptions, MPCType, + verifyEddsaTssWalletAddress, } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, CoinFamily } from '@bitgo/statics'; import utils from './lib/utils'; @@ -92,29 +91,11 @@ export class Iota extends BaseCoin { * @param params */ async isWalletAddress(params: TssVerifyAddressOptions): Promise { - const { keychains, address, index } = params; - - if (!this.isValidAddress(address)) { - throw new InvalidAddressError(`invalid address: ${address}`); - } - - if (!keychains) { - throw new Error('missing required param keychains'); - } - - for (const keychain of keychains) { - const MPC = await EDDSAMethods.getInitializedMpcInstance(); - const commonKeychain = keychain.commonKeychain as string; - - const derivationPath = 'm/' + index; - const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64); - const expectedAddress = utils.getAddressFromPublicKey(derivedPublicKey); - - if (address !== expectedAddress) { - return false; - } - } - return true; + return verifyEddsaTssWalletAddress( + params, + (address) => this.isValidAddress(address), + (publicKey) => utils.getAddressFromPublicKey(publicKey) + ); } /** diff --git a/modules/sdk-coin-sui/src/sui.ts b/modules/sdk-coin-sui/src/sui.ts index 896aa0871c..5c6fb054b2 100644 --- a/modules/sdk-coin-sui/src/sui.ts +++ b/modules/sdk-coin-sui/src/sui.ts @@ -7,7 +7,6 @@ import { EDDSAMethods, EDDSAMethodTypes, Environments, - InvalidAddressError, KeyPair, MPCAlgorithm, MPCRecoveryOptions, @@ -30,6 +29,7 @@ import { MultisigType, multisigTypes, AuditDecryptedKeyParams, + verifyEddsaTssWalletAddress, } from '@bitgo/sdk-core'; import { BaseCoin as StaticsBaseCoin, BaseNetwork, coins, SuiCoin } from '@bitgo/statics'; import BigNumber from 'bignumber.js'; @@ -188,12 +188,11 @@ export class Sui extends BaseCoin { } async isWalletAddress(params: TssVerifyAddressOptions): Promise { - const { address: newAddress } = params; - - if (!this.isValidAddress(newAddress)) { - throw new InvalidAddressError(`invalid address: ${newAddress}`); - } - return true; + return verifyEddsaTssWalletAddress( + params, + (address) => this.isValidAddress(address), + (publicKey) => this.getAddressFromPublicKey(publicKey) + ); } async parseTransaction(params: SuiParseTransactionOptions): Promise { diff --git a/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts b/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts index 11362f59ab..6de6577ba7 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts @@ -1,4 +1,5 @@ -import { InvalidAddressError, TssVerifyAddressOptions } from '../../baseCoin/iBaseCoin'; +import { TssVerifyAddressOptions } from '../../baseCoin/iBaseCoin'; +import { InvalidAddressError } from '../../errors'; import { EDDSAMethods } from '../../tss'; /** From a64baf34ab44924b14930d920ee1db713a7cc896 Mon Sep 17 00:00:00 2001 From: Pranav Jain Date: Wed, 22 Oct 2025 18:02:37 -0400 Subject: [PATCH 5/5] chore: use Keychains and extract commonKeychain verfn TICKET: WP-6378 --- modules/sdk-coin-dot/test/unit/dot.ts | 31 ++----------- modules/sdk-coin-sol/test/unit/sol.ts | 31 ++----------- modules/sdk-coin-ton/src/ton.ts | 31 ++++++------- .../sdk-core/src/bitgo/baseCoin/iBaseCoin.ts | 20 +++++++-- .../bitgo/utils/tss/addressVerification.ts | 43 +++++++++++-------- 5 files changed, 62 insertions(+), 94 deletions(-) diff --git a/modules/sdk-coin-dot/test/unit/dot.ts b/modules/sdk-coin-dot/test/unit/dot.ts index 64a22b20aa..21a618e538 100644 --- a/modules/sdk-coin-dot/test/unit/dot.ts +++ b/modules/sdk-coin-dot/test/unit/dot.ts @@ -678,7 +678,7 @@ describe('DOT:', function () { const commonKeychain = '6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e'; const index = '3'; - const keychains = [{ commonKeychain }]; + const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }]; const result = await basecoin.isWalletAddress({ keychains, address, index }); result.should.equal(true); @@ -689,7 +689,7 @@ describe('DOT:', function () { const wrongKeychain = '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; const index = '3'; - const keychains = [{ commonKeychain: wrongKeychain }]; + const keychains = [{ id: '1', type: 'tss' as const, commonKeychain: wrongKeychain }]; const result = await basecoin.isWalletAddress({ keychains, address, index }); result.should.equal(false); @@ -700,7 +700,7 @@ describe('DOT:', function () { const commonKeychain = '6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e'; const wrongIndex = '999'; - const keychains = [{ commonKeychain }]; + const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }]; const result = await basecoin.isWalletAddress({ keychains, address, index: wrongIndex }); result.should.equal(false); @@ -711,35 +711,12 @@ describe('DOT:', function () { const commonKeychain = '6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e'; const index = '3'; - const keychains = [{ commonKeychain }]; + const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }]; await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address: invalidAddress, index }), { message: `invalid address: ${invalidAddress}`, }); }); - - it('should throw error when keychains are missing', async function () { - const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74'; - const index = '3'; - - await assert.rejects(async () => await basecoin.isWalletAddress({ address, index } as any), { - message: 'missing required param keychains', - }); - }); - - it('should throw error when keychains have different commonKeychains', async function () { - const address = '5DxD9nT16GQLrU6aB5pSS5VtxoZbVju3NHUCcawxZyZCTf74'; - const commonKeychain1 = - '6d2d5150f6e435dfd9b4f225f2cc29d95ec3b61b34e8bec98693b1a7ffe44cd764f99ee5058838d785c73360ad4f24d78e0255ab2c368c09060b29a9b27f040e'; - const commonKeychain2 = - '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; - const index = '3'; - const keychains = [{ commonKeychain: commonKeychain1 }, { commonKeychain: commonKeychain2 }]; - - await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address, index }), { - message: 'all keychains must have the same commonKeychain for MPC coins', - }); - }); }); describe('getAddressFromPublicKey', () => { diff --git a/modules/sdk-coin-sol/test/unit/sol.ts b/modules/sdk-coin-sol/test/unit/sol.ts index f089793aa9..0306b4095e 100644 --- a/modules/sdk-coin-sol/test/unit/sol.ts +++ b/modules/sdk-coin-sol/test/unit/sol.ts @@ -3360,7 +3360,7 @@ describe('SOL:', function () { const commonKeychain = '8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd'; const index = '1'; - const keychains = [{ commonKeychain }]; + const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }]; const result = await basecoin.isWalletAddress({ keychains, address, index }); result.should.equal(true); @@ -3371,7 +3371,7 @@ describe('SOL:', function () { const wrongKeychain = '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; const index = '1'; - const keychains = [{ commonKeychain: wrongKeychain }]; + const keychains = [{ id: '1', type: 'tss' as const, commonKeychain: wrongKeychain }]; const result = await basecoin.isWalletAddress({ keychains, address, index }); result.should.equal(false); @@ -3382,7 +3382,7 @@ describe('SOL:', function () { const commonKeychain = '8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd'; const wrongIndex = '999'; - const keychains = [{ commonKeychain }]; + const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }]; const result = await basecoin.isWalletAddress({ keychains, address, index: wrongIndex }); result.should.equal(false); @@ -3393,35 +3393,12 @@ describe('SOL:', function () { const commonKeychain = '8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd'; const index = '1'; - const keychains = [{ commonKeychain }]; + const keychains = [{ id: '1', type: 'tss' as const, commonKeychain }]; await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address: invalidAddress, index }), { message: `invalid address: ${invalidAddress}`, }); }); - - it('should throw error when keychains are missing', async function () { - const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY'; - const index = '1'; - - await assert.rejects(async () => await basecoin.isWalletAddress({ address, index } as any), { - message: 'missing required param keychains', - }); - }); - - it('should throw error when keychains have different commonKeychains', async function () { - const address = '7YAesfwPk41VChUgr65bm8FEep7ymWqLSW5rpYB5zZPY'; - const commonKeychain1 = - '8ea32ecacfc83effbd2e2790ee44fa7c59b4d86c29a12f09fb613d8195f93f4e21875cad3b98adada40c040c54c3569467df41a020881a6184096378701862bd'; - const commonKeychain2 = - '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; - const index = '1'; - const keychains = [{ commonKeychain: commonKeychain1 }, { commonKeychain: commonKeychain2 }]; - - await assert.rejects(async () => await basecoin.isWalletAddress({ keychains, address, index }), { - message: 'all keychains must have the same commonKeychain for MPC coins', - }); - }); }); describe('getAddressFromPublicKey', () => { diff --git a/modules/sdk-coin-ton/src/ton.ts b/modules/sdk-coin-ton/src/ton.ts index ffe7629120..6553e1f566 100644 --- a/modules/sdk-coin-ton/src/ton.ts +++ b/modules/sdk-coin-ton/src/ton.ts @@ -30,6 +30,7 @@ import { MPCTxs, MPCSweepRecoveryOptions, AuditDecryptedKeyParams, + extractCommonKeychain, } from '@bitgo/sdk-core'; import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc'; import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics'; @@ -159,29 +160,21 @@ export class Ton extends BaseCoin { throw new InvalidAddressError(`invalid address: ${newAddress}`); } - if (!keychains) { - throw new Error('missing required param keychains'); - } - - for (const keychain of keychains) { - const [address, memoId] = newAddress.split('?memoId='); - const MPC = await EDDSAMethods.getInitializedMpcInstance(); - const commonKeychain = keychain.commonKeychain as string; + const [address, memoId] = newAddress.split('?memoId='); - const derivationPath = 'm/' + index; - const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64); - const expectedAddress = await Utils.default.getAddressFromPublicKey(derivedPublicKey); + // TON supports memoId for address tagging - verify it matches the index + if (memoId) { + return memoId === `${index}`; + } - if (memoId) { - return memoId === `${index}`; - } + const commonKeychain = extractCommonKeychain(keychains); - if (address !== expectedAddress) { - return false; - } - } + const MPC = await EDDSAMethods.getInitializedMpcInstance(); + const derivationPath = 'm/' + index; + const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64); + const expectedAddress = await Utils.default.getAddressFromPublicKey(derivedPublicKey); - return true; + return address === expectedAddress; } async parseTransaction(params: TonParseTransactionOptions): Promise { diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index b0add6a9fd..c4ad7f62f1 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -155,12 +155,24 @@ export interface VerifyAddressOptions { impliedForwarderVersion?: number; } +/** + * Options for verifying if an address belongs to a TSS/MPC wallet. + * Used for EdDSA-based MPC coins (SOL, DOT, SUI, TON, IOTA, NEAR, etc.) + * to cryptographically verify address derivation without trusting the platform. + */ export interface TssVerifyAddressOptions { + /** The address to verify */ address: string; - keychains: { - commonKeychain: string; - }[]; - chain?: string; + /** + * Keychains containing the commonKeychain for HD derivation. + * For MPC wallets, the commonKeychain (combined public key from MPC key generation) + * should be identical across all keychains (user, backup, bitgo). + */ + keychains: Keychain[]; + /** + * Derivation index for the address. + * Used to derive child addresses from the root keychain via HD derivation path: m/{index} + */ index: string; } diff --git a/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts b/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts index 6de6577ba7..cc42b95f14 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts @@ -2,6 +2,31 @@ import { TssVerifyAddressOptions } from '../../baseCoin/iBaseCoin'; import { InvalidAddressError } from '../../errors'; import { EDDSAMethods } from '../../tss'; +/** + * Extracts and validates the commonKeychain from keychains array. + * For MPC wallets, all keychains should have the same commonKeychain. + * + * @param keychains - Array of keychains containing commonKeychain + * @returns The validated commonKeychain + * @throws {Error} if keychains are missing, empty, or have mismatched commonKeychains + */ +export function extractCommonKeychain(keychains: TssVerifyAddressOptions['keychains']): string { + if (!keychains?.length) { + throw new Error('missing required param keychains'); + } + + const commonKeychain = keychains[0].commonKeychain; + if (!commonKeychain) { + throw new Error('missing required param commonKeychain'); + } + + // Verify all keychains have the same commonKeychain + if (keychains.find((kc) => kc.commonKeychain !== commonKeychain)) + throw new Error('all keychains must have the same commonKeychain for MPC coins'); + + return commonKeychain; +} + /** * Verifies if an address belongs to a wallet using EdDSA TSS MPC derivation. * This is a common implementation for EdDSA-based MPC coins (SOL, DOT, SUI, TON, IOTA, etc.) @@ -24,24 +49,8 @@ export async function verifyEddsaTssWalletAddress( throw new InvalidAddressError(`invalid address: ${address}`); } - if (!keychains || keychains.length === 0) { - throw new Error('missing required param keychains'); - } - - // For MPC coins, commonKeychain should be the same for all keychains - const commonKeychain = keychains[0].commonKeychain as string; - if (!commonKeychain) { - throw new Error('missing required param commonKeychain'); - } - - // Verify all keychains have the same commonKeychain - for (const keychain of keychains) { - if (keychain.commonKeychain !== commonKeychain) { - throw new Error('all keychains must have the same commonKeychain for MPC coins'); - } - } + const commonKeychain = extractCommonKeychain(keychains); - // Only perform derivation once since commonKeychain is the same const MPC = await EDDSAMethods.getInitializedMpcInstance(); const derivationPath = 'm/' + index; const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);