Skip to content
Draft
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
40 changes: 40 additions & 0 deletions modules/abstract-eth/src/abstractEthLikeNewCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ import {
VerifyTransactionOptions,
Wallet,
verifyMPCWalletAddress,
deriveMPCWalletAddress,
TssVerifyAddressOptions,
isTssVerifyAddressOptions,
DeriveAddressOptions,
DeriveAddressResult,
} from '@bitgo/sdk-core';
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
import { bip32 } from '@bitgo/secp256k1';
Expand Down Expand Up @@ -3040,6 +3043,43 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
throw new Error(`Base address verification not supported for wallet version ${params.walletVersion}`);
}

/**
* Locally derive an ETH wallet receive address from a derivation path, using the wallet's
* commonKeychain only (no private keys, no network access). The inverse of
* {@link isWalletAddress}: it *produces* the address via the same secp256k1 MPC derivation
* that isWalletAddress checks against (`deriveMPCWalletAddress` + `KeyPair.getAddress()`),
* so derive and verify can never diverge.
*
* Supports MPC/TSS wallets only (wallet versions 3, 5, 6). Legacy BIP32 forwarder wallets
* (versions 1, 2, 4) derive per-index forwarder contract addresses and are handled separately.
* @param params keychains (commonKeychain), derivation index, walletVersion, and optional SMC seed
* @returns the derived address, the index used, and the HD derivation path
*/
async deriveAddress(params: DeriveAddressOptions): Promise<DeriveAddressResult> {
const isMpcWallet = params.walletVersion === 3 || params.walletVersion === 5 || params.walletVersion === 6;
if (!isMpcWallet) {
throw new Error(
`deriveAddress currently supports only MPC/TSS ETH wallets (wallet versions 3, 5, 6). ` +
`Legacy BIP32 forwarder wallets (versions 1, 2, 4) are not yet supported. ` +
`Got walletVersion ${params.walletVersion}.`
);
}

const { address, derivationPath } = await deriveMPCWalletAddress(
{
// extractCommonKeychain validates the commonKeychain is present at runtime
keychains: (params.keychains ?? []) as TssVerifyAddressOptions['keychains'],
index: params.index,
derivedFromParentWithSeed: params.derivedFromParentWithSeed,
multisigTypeVersion: params.multisigTypeVersion,
keyCurve: 'secp256k1',
},
(pubKey) => new KeyPairLib({ pub: pubKey }).getAddress()
);

return { address, index: params.index, derivationPath };
}

/**
*
* @param {TransactionPrebuild} txPrebuild
Expand Down
50 changes: 50 additions & 0 deletions modules/sdk-coin-eth/test/unit/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,56 @@ describe('ETH:', function () {
});
});

describe('deriveAddress (MPC)', function () {
// Same vector as the MPC isWalletAddress tests above:
// commonKeychain @ index 0 -> 0x01153f3adfe454a72589ca9ef74f013c19e54961
const commonKeychain =
'03f9c2fb2e5a8b78a44f5d1e4f906f8e3d7a0e6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9e8d7c6b5a4' +
'93827160594857463728190a0b0c0d0e0f101112131415161718191a1b1c1d1e1f';
const keychains = [
{ pub: 'user_pub', commonKeychain },
{ pub: 'backup_pub', commonKeychain },
{ pub: 'bitgo_pub', commonKeychain },
];

it('should derive the expected MPC address for a commonKeychain at index 0', async function () {
const coin = bitgo.coin('teth') as Teth;
const result = await coin.deriveAddress({ keychains, index: 0, walletVersion: 3 });
result.address.should.equal('0x01153f3adfe454a72589ca9ef74f013c19e54961');
result.index.should.equal(0);
assert.strictEqual(result.derivationPath, 'm/0');
});

it('round-trips with isWalletAddress (derived address verifies as true)', async function () {
const coin = bitgo.coin('teth') as Teth;
const derived = await coin.deriveAddress({ keychains, index: 3, walletVersion: 6 });
const verified = await coin.isWalletAddress({
address: derived.address,
coinSpecific: { forwarderVersion: 5 },
keychains,
index: 3,
walletVersion: 6,
} as unknown as TssVerifyEthAddressOptions);
verified.should.equal(true);
});

it('should throw for legacy BIP32 forwarder wallet versions (1/2/4)', async function () {
const coin = bitgo.coin('teth') as Teth;
await assert.rejects(
async () => coin.deriveAddress({ keychains, index: 0, walletVersion: 1 }),
/only MPC\/TSS ETH wallets/
);
});

it('should throw if keychains are missing', async function () {
const coin = bitgo.coin('teth') as Teth;
await assert.rejects(
async () => coin.deriveAddress({ keychains: [], index: 0, walletVersion: 3 }),
/keychains/
);
});
});

describe('Base Address Verification', function () {
it('should verify base address for wallet version 6 (TSS)', async function () {
const coin = bitgo.coin('hteth') as Hteth;
Expand Down
Loading