Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ export interface VerifyAddressOptions {
* For SMC (Self-Managed Custodial) TSS wallets, this is used to compute the derivation prefix.
*/
derivedFromParentWithSeed?: string;
/**
* Identifies the MPC signing protocol version of the wallet (e.g. 'MPCv2').
* Used to distinguish between MPCv1 and MPCv2 wallets.
*/
multisigTypeVersion?: 'MPCv2';
}

/**
Expand Down Expand Up @@ -187,6 +192,11 @@ export interface TssVerifyAddressOptions {
* The derivation path becomes {computedPrefix}/{index} instead of m/{index}.
*/
derivedFromParentWithSeed?: string;
/**
* Identifies the MPC signing protocol version of the wallet (e.g. 'MPCv2').
* Used to distinguish between MPCv1 and MPCv2 wallets.
*/
multisigTypeVersion?: 'MPCv2';
}

export function isTssVerifyAddressOptions<T extends VerifyAddressOptions | TssVerifyAddressOptions>(
Expand Down
13 changes: 10 additions & 3 deletions modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
import { getDerivationPath, deriveUnhardenedMps } from '@bitgo/sdk-lib-mpc';
import { Ecdsa } from '../../../account-lib/mpc';
import { TssVerifyAddressOptions } from '../../baseCoin/iBaseCoin';
import { InvalidAddressError } from '../../errors';
Expand Down Expand Up @@ -72,15 +72,22 @@ export async function verifyMPCWalletAddress(
throw new InvalidAddressError(`invalid address: ${address}`);
}

const MPC = params.keyCurve === 'secp256k1' ? new Ecdsa() : await EDDSAMethods.getInitializedMpcInstance();
const commonKeychain = extractCommonKeychain(keychains);

// Compute derivation path:
// - For SMC wallets with derivedFromParentWithSeed, compute prefix and use: {prefix}/{index}
// - For other wallets, use simple path: m/{index}
const prefix = derivedFromParentWithSeed ? getDerivationPath(derivedFromParentWithSeed.toString()) : undefined;
const derivationPath = prefix ? `${prefix}/${index}` : `m/${index}`;
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath);

// MPCv2 EdDSA wallets use a different BIP32-Ed25519 derivation formula than MPCv1 wallets.
let derivedPublicKey: string;
if (params.keyCurve === 'ed25519' && params.multisigTypeVersion === 'MPCv2') {
derivedPublicKey = deriveUnhardenedMps(commonKeychain, derivationPath);
} else {
const MPC = params.keyCurve === 'secp256k1' ? new Ecdsa() : await EDDSAMethods.getInitializedMpcInstance();
derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath);
}

// secp256k1 expects 33 bytes; ed25519 expects 32 bytes
const publicKeySize = params.keyCurve === 'secp256k1' ? 33 : 32;
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-core/src/bitgo/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1413,6 +1413,7 @@ export class Wallet implements IWallet {
const verificationData: VerifyAddressOptions = _.merge({}, newAddress, {
rootAddress,
walletVersion: _.get(this._wallet, 'coinSpecific.walletVersion'),
multisigTypeVersion: this.multisigTypeVersion(),
});

if (verificationData.error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import * as assert from 'assert';
import 'should';
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
import { deriveUnhardenedMps, getDerivationPath } from '@bitgo/sdk-lib-mpc';

function getAddressVerificationModule() {
return require('../../../../../src/bitgo/utils/tss/addressVerification');
}

const getExtractCommonKeychain = () => getAddressVerificationModule().extractCommonKeychain;
const getVerifyEddsaTssWalletAddress = () => getAddressVerificationModule().verifyEddsaTssWalletAddress;

// RFC 8032 test vector: known valid Ed25519 public key + arbitrary chaincode = 128 hex chars.
const TEST_PK = 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a';
const TEST_CHAINCODE = 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef';
const TEST_KEYCHAIN = TEST_PK + TEST_CHAINCODE;

describe('TSS Address Verification - Derivation Path with Prefix', function () {
const commonKeychain =
Expand Down Expand Up @@ -61,3 +67,81 @@ describe('TSS Address Verification - Derivation Path with Prefix', function () {
});
});
});

describe('verifyEddsaTssWalletAddress', function () {
const keychains = [
{ commonKeychain: TEST_KEYCHAIN },
{ commonKeychain: TEST_KEYCHAIN },
{ commonKeychain: TEST_KEYCHAIN },
];
const isValidAddress = (addr: string) => addr.length === 64;
const getAddressFromPublicKey = (pk: string) => pk;

describe('MPCv2 wallets (Silence Labs / MPS formula)', function () {
it('verifies a correct address derived with deriveUnhardenedMps at index 0', async function () {
const verifyEddsaTssWalletAddress = getVerifyEddsaTssWalletAddress();
const expectedAddress = deriveUnhardenedMps(TEST_KEYCHAIN, 'm/0').slice(0, 64);

const result = await verifyEddsaTssWalletAddress(
{ address: expectedAddress, keychains, index: 0, multisigTypeVersion: 'MPCv2' },
isValidAddress,
getAddressFromPublicKey
);
result.should.be.true();
});

it('verifies a correct address derived with deriveUnhardenedMps at index 1', async function () {
const verifyEddsaTssWalletAddress = getVerifyEddsaTssWalletAddress();
const expectedAddress = deriveUnhardenedMps(TEST_KEYCHAIN, 'm/1').slice(0, 64);

const result = await verifyEddsaTssWalletAddress(
{ address: expectedAddress, keychains, index: 1, multisigTypeVersion: 'MPCv2' },
isValidAddress,
getAddressFromPublicKey
);
result.should.be.true();
});

it('rejects an address derived at a different index', async function () {
const verifyEddsaTssWalletAddress = getVerifyEddsaTssWalletAddress();
const addressFromIndex0 = deriveUnhardenedMps(TEST_KEYCHAIN, 'm/0').slice(0, 64);

const result = await verifyEddsaTssWalletAddress(
{ address: addressFromIndex0, keychains, index: 1, multisigTypeVersion: 'MPCv2' },
isValidAddress,
getAddressFromPublicKey
);
result.should.be.false();
});

it('rejects a random address that was not derived from the keychain', async function () {
const verifyEddsaTssWalletAddress = getVerifyEddsaTssWalletAddress();
const randomAddress = 'ab'.repeat(32); // 64 hex chars, wrong address

const result = await verifyEddsaTssWalletAddress(
{ address: randomAddress, keychains, index: 0, multisigTypeVersion: 'MPCv2' },
isValidAddress,
getAddressFromPublicKey
);
result.should.be.false();
});
});

describe('non-MPCv2 wallets (MPCv1 formula)', function () {
it('rejects an MPCv2-derived address when multisigTypeVersion is not set', async function () {
const verifyEddsaTssWalletAddress = getVerifyEddsaTssWalletAddress();
// MPCv2 (Silence Labs) and MPCv1 formulas produce different addresses for the same keychain.
// Without multisigTypeVersion: 'MPCv2', the MPCv1 formula is used, so the MPCv2-derived
// address should not match.
const mpcv2Address = deriveUnhardenedMps(TEST_KEYCHAIN, 'm/0').slice(0, 64);

const result = await verifyEddsaTssWalletAddress(
{ address: mpcv2Address, keychains, index: 0 },
isValidAddress,
getAddressFromPublicKey
);

result.should.be.false();
});
});
});