Skip to content

Commit

Permalink
feat(root): implement Eddsa multisig derivation
Browse files Browse the repository at this point in the history
Implemented Eddsa multisig derivation

BREAKING CHANGE: wallet.getPrv() is now an async method, coin.generateKeyPair(),
coin.deriveKeyWithSeed() and keychains.create() are deprecated, use their Async version instead.

WP-1401

TICKET: WP-1401
  • Loading branch information
alebusse committed Feb 12, 2024
1 parent 2d3e345 commit 09c61bd
Show file tree
Hide file tree
Showing 29 changed files with 447 additions and 199 deletions.
4 changes: 2 additions & 2 deletions modules/abstract-eth/src/abstractEthLikeNewCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2011,7 +2011,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
const walletPassphrase = buildParams.walletPassphrase;

const userKeychain = await this.keychains().get({ id: wallet.keyIds()[0] });
const userPrv = wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });
const userPrv = await wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });
const userPrvBuffer = bip32.fromBase58(userPrv).privateKey;
if (!userPrvBuffer) {
throw new Error('invalid userPrv');
Expand Down Expand Up @@ -2222,7 +2222,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
* @param {Buffer} seed
* @returns {KeyPair} object with generated pub and prv
*/
generateKeyPair(seed: Buffer): KeyPair {
generateKeyPair(seed?: Buffer): KeyPair {
if (!seed) {
// An extended private key has both a normal 256 bit private key and a 256
// bit chain code, both of which must be random. 512 bits is therefore the
Expand Down
17 changes: 17 additions & 0 deletions modules/account-lib/test/resources/eddsaKeyDeriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const data = {
seed: {
validSeed:
'e3e670f488500f300790bee3fc2fb075d82d279459fa53024168664aec73054f2f5a0abfcd5889100604b0c22a430c4f9e1ac92b43675ca7cef0f9550acd2afa',
expectedRootKeys: {
prv: 'rprvb047f0d8b29083e1933102983d3c611cbf3364039cf3c985730e242488936070:2f5a0abfcd5889100604b0c22a430c4f9e1ac92b43675ca7cef0f9550acd2afa:049aa12f8beaf88eb1a404e4fc5a7ad290accd5b6bb37fa497f2e3bd56fa5990',
pub: 'rpubf008e38df59581e2851939f035bebbb18755fcb9d3864eb2aaa22be060ede6:2f5a0abfcd5889100604b0c22a430c4f9e1ac92b43675ca7cef0f9550acd2afa',
},
},
rootKeys1: {
prv: 'rprvb00584090478b47d1d1bf45ee8bbd012ec3634ec5eb40fb944c22602daac8979:427b2e97eeab94519d08e651b48d3df9326e9b22615f7655ad8c256659ec45af:838b4de99dcaf2392cec3222b29146816b1d3dfc1a7cad0695959a53ec23fa77',
pub: 'rpub6b0de9d546d71d95a04a9cfe0b6b8cd2f5da4961c6170ff5e8b7d27098e3a909:427b2e97eeab94519d08e651b48d3df9326e9b22615f7655ad8c256659ec45af',
derivedPub: '8beba02ca52fcb799134bc5698ee7f2b9e4e254749ea6baa65d7ddfbab82e330',
derivedPrv:
'dd6122533efd21fece9248f54e82b812d16996b47726f8cadc2a20f1ddac89098beba02ca52fcb799134bc5698ee7f2b9e4e254749ea6baa65d7ddfbab82e330',
},
};
74 changes: 74 additions & 0 deletions modules/account-lib/test/unit/utils/eddsaKeyDeriver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import assert from 'assert';
import { EddsaKeyDeriver } from '@bitgo/sdk-core';
import { data } from '../../resources/eddsaKeyDeriver';

describe('EddsaKeyDeriver', () => {
describe('createRootKeys', () => {
it('should create root keys without seed', async () => {
const rootKeys = await EddsaKeyDeriver.createRootKeys();

assert.ok(rootKeys.prv);
assert.equal(rootKeys.prv.length, data.rootKeys1.prv.length);
assert.ok(rootKeys.prv.startsWith(EddsaKeyDeriver.ROOT_PRV_KEY_PREFIX));
assert.ok(rootKeys.pub);
assert.equal(rootKeys.pub.length, data.rootKeys1.pub.length);
assert.ok(rootKeys.pub.startsWith(EddsaKeyDeriver.ROOT_PUB_KEY_PREFIX));
});

it('should create root keys with seed', async () => {
const seed = Buffer.from(data.seed.validSeed, 'hex');
const rootKeys = await EddsaKeyDeriver.createRootKeys(seed);

assert.ok(rootKeys.prv);
assert.equal(rootKeys.prv, data.seed.expectedRootKeys.prv);
assert.ok(rootKeys.pub);
assert.equal(rootKeys.pub, data.seed.expectedRootKeys.pub);
});

it('should throw for invalid seed', async () => {
const seed = Buffer.from('asdf12f1', 'hex');

assert.rejects(
async () => {
await EddsaKeyDeriver.createRootKeys(seed);
},
{ message: 'Invalid seed' },
);
});
});

describe('deriveKeyWithSeed', () => {
const seed = 'seed123';
const expectedPath = 'm/999999/240510315/85914100';
it('should derive a pub key and path from for root public key with seed', async () => {
const rootPubKey = data.rootKeys1.pub;

const derivedKey = await EddsaKeyDeriver.deriveKeyWithSeed(rootPubKey, seed);

assert.equal(derivedKey.key.length, 64);
assert.equal(derivedKey.key, data.rootKeys1.derivedPub);
assert.equal(derivedKey.derivationPath, expectedPath);
});

it('should derive a private key and path from for root private key with seed', async () => {
const rootPrvKey = data.rootKeys1.prv;

const derivedKey = await EddsaKeyDeriver.deriveKeyWithSeed(rootPrvKey, seed);

assert.equal(derivedKey.key.length, 128);
assert.equal(derivedKey.key, data.rootKeys1.derivedPrv);
assert.equal(derivedKey.derivationPath, expectedPath);
});

it('should throw an error for invalid key format', async () => {
const invalidKey = 'invalid:key:format';

await assert.rejects(
async () => {
await EddsaKeyDeriver.deriveKeyWithSeed(invalidKey, seed);
},
{ message: 'Invalid key format' },
);
});
});
});
2 changes: 1 addition & 1 deletion modules/bitgo/test/v2/unit/internal/tssUtils/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ describe('TSS Ecdsa Utils:', async function () {
});

// Seems to be flaky on CI, failed here: https://github.com/BitGo/BitGoJS/actions/runs/5902489990/job/16010623888?pr=3822
it.skip('createOfflineMuDeltaShare should succeed', async function () {
xit('createOfflineMuDeltaShare should succeed', async function () {
const mockPassword = 'password';
const alphaLength = 1536;
const deltaLength = 64;
Expand Down
8 changes: 4 additions & 4 deletions modules/bitgo/test/v2/unit/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ describe('V2 Wallet:', function () {
prv,
coldDerivationSeed: '123',
};
wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv);
(await wallet.getUserPrv(userPrvOptions)).should.eql(derivedPrv);
});

it('should use the user keychain derivedFromParentWithSeed as the cold derivation seed if none is provided', async () => {
Expand All @@ -337,7 +337,7 @@ describe('V2 Wallet:', function () {
type: 'independent',
},
};
wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv);
(await wallet.getUserPrv(userPrvOptions)).should.eql(derivedPrv);
});

it('should prefer the explicit cold derivation seed to the user keychain derivedFromParentWithSeed', async () => {
Expand All @@ -351,7 +351,7 @@ describe('V2 Wallet:', function () {
type: 'independent',
},
};
wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv);
(await wallet.getUserPrv(userPrvOptions)).should.eql(derivedPrv);
});

it('should return the prv provided for TSS SMC', async () => {
Expand Down Expand Up @@ -379,7 +379,7 @@ describe('V2 Wallet:', function () {
prv,
keychain,
};
wallet.getUserPrv(userPrvOptions).should.eql(prv);
(await wallet.getUserPrv(userPrvOptions)).should.eql(prv);
});
});

Expand Down
2 changes: 1 addition & 1 deletion modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ export async function handleV2Sign(req: express.Request) {
let privKey = decryptPrivKey(bitgo, encryptedPrivKey, walletPw);
const coin = bitgo.coin(req.params.coin);
if (req.body.derivationSeed) {
privKey = coin.deriveKeyWithSeed({ key: privKey, seed: req.body.derivationSeed }).key;
privKey = (await coin.deriveKeyWithSeed({ key: privKey, seed: req.body.derivationSeed })).key;
}
try {
return await coin.signTransaction({ ...req.body, prv: privKey });
Expand Down
1 change: 0 additions & 1 deletion modules/sdk-coin-algo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"dependencies": {
"@bitgo/sdk-core": "^25.1.0",
"@bitgo/statics": "^46.1.0",
"@bitgo/utxo-lib": "^9.34.0",
"@hashgraph/cryptography": "1.1.2",
"@stablelib/hex": "^1.0.0",
"algosdk": "1.14.0",
Expand Down
55 changes: 30 additions & 25 deletions modules/sdk-coin-algo/src/algo.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/**
* @prettier
*/
import * as utxolib from '@bitgo/utxo-lib';
import * as _ from 'lodash';
import { SeedValidator } from './seedValidator';
import { coins, CoinFamily } from '@bitgo/statics';
Expand All @@ -10,7 +9,6 @@ import {
AddressCoinSpecific,
BaseCoin,
BitGoBase,
Ed25519KeyDeriver,
InvalidAddressError,
InvalidKey,
KeyIndices,
Expand All @@ -26,6 +24,9 @@ import {
UnexpectedAddressError,
VerifyAddressOptions,
VerifyTransactionOptions,
EddsaKeyDeriver,
NotImplementedError,
GenerateKeyPairAsyncOptions,
} from '@bitgo/sdk-core';
import stellar from 'stellar-sdk';

Expand Down Expand Up @@ -169,12 +170,23 @@ export class Algo extends BaseCoin {
return true;
}

/**
* Generate ed25519 key pair
*
* @param seed
* @returns {Object} object with generated pub, prv
*/
/** inheritdoc */
deriveKeyWithSeed(): { key: string; derivationPath: string } {
throw new NotImplementedError('use deriveKeyWithSeedAsync instead');
}

/** inheritdoc */
async deriveKeyWithSeedAsync({
key,
seed,
}: {
key: string;
seed: string;
}): Promise<{ key: any; derivationPath: string }> {
return await EddsaKeyDeriver.deriveKeyWithSeed(key, seed);
}

/** inheritdoc */
generateKeyPair(seed?: Buffer): KeyPair {
const keyPair = seed ? new AlgoLib.KeyPair({ seed }) : new AlgoLib.KeyPair();
const keys = keyPair.getKeys();
Expand All @@ -188,6 +200,16 @@ export class Algo extends BaseCoin {
};
}

/** inheritdoc */
async generateKeyPairAsync(options: GenerateKeyPairAsyncOptions = {}): Promise<KeyPair> {
const { seed, rootKey } = options;
if (rootKey) {
const keypair = await EddsaKeyDeriver.createRootKeys(seed);
return keypair;
}
return this.generateKeyPair(seed);
}

/**
* Return boolean indicating whether input is valid public key for the coin.
*
Expand Down Expand Up @@ -504,23 +526,6 @@ export class Algo extends BaseCoin {
return true;
}

/** @inheritDoc */
deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): { derivationPath: string; key: string } {
const derivationPathInput = utxolib.crypto.hash256(Buffer.from(seed, 'utf8')).toString('hex');
const derivationPathParts = [
999999,
parseInt(derivationPathInput.slice(0, 7), 16),
parseInt(derivationPathInput.slice(7, 14), 16),
];
const derivationPath = 'm/' + derivationPathParts.map((part) => `${part}'`).join('/');
const derivedKey = Ed25519KeyDeriver.derivePath(derivationPath, key).key;
const keypair = new AlgoLib.KeyPair({ seed: derivedKey });
return {
key: keypair.getAddress(),
derivationPath,
};
}

decodeTx(txn: Buffer): unknown {
return AlgoLib.algoUtils.decodeAlgoTxn(txn);
}
Expand Down
30 changes: 12 additions & 18 deletions modules/sdk-coin-algo/test/unit/algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ describe('ALGO:', function () {
});
});

describe('sync methods error handling', () => {
it('should throw error for deriveKeyWithSeed()', () => {
(() => {
basecoin.deriveKeyWithSeed();
}).should.throw('use deriveKeyWithSeedAsync instead');
});
});

describe('Transfer Builder: ', () => {
const buildBaseTransferTransaction = ({ destination, amount = 10000, sender, memo = '' }) => {
const factory = new AlgoLib.TransactionBuilderFactory(coins.get('algo'));
Expand Down Expand Up @@ -608,32 +616,18 @@ describe('ALGO:', function () {
});

describe('Generate wallet key pair: ', () => {
it('should generate key pair', () => {
const kp = basecoin.generateKeyPair();
it('should generate key pair', async () => {
const kp = await basecoin.generateKeyPairAsync();
basecoin.isValidPub(kp.pub).should.equal(true);
basecoin.isValidPrv(kp.prv).should.equal(true);
});

it('should generate key pair from seed', () => {
it('should generate key pair from seed', async () => {
const seed = Buffer.from('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60', 'hex');
const kp = basecoin.generateKeyPair(seed);
const kp = await basecoin.generateKeyPairAsync({ seed });
basecoin.isValidPub(kp.pub).should.equal(true);
basecoin.isValidPrv(kp.prv).should.equal(true);
});

it('should deterministically derive keypair with seed', () => {
const derivedKeypair = basecoin.deriveKeyWithSeed({
key: 'UBI2KNGT742KGIPHMZDJHHSADIT56HRDPVOOCCRYIETD4BAJLCBMQNSCNE',
seed: 'cold derivation seed',
});
console.log(JSON.stringify(derivedKeypair));

basecoin.isValidPub(derivedKeypair.key).should.be.true();
derivedKeypair.should.deepEqual({
key: 'NAYUBR4HQKJBBTNNKXQIY7GMHUCHVAUH5DQ4ZVHVL67CUQLGHRWDKQAHYU',
derivationPath: "m/999999'/25725073'/5121434'",
});
});
});

describe('Enable, disable and transfer Token ', () => {
Expand Down
6 changes: 3 additions & 3 deletions modules/sdk-coin-btc/src/inscriptionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
const user = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] });
assert(user.pub);

const derived = this.coin.deriveKeyWithSeed({ key: user.pub, seed: inscriptionData.toString() });
const derived = await this.coin.deriveKeyWithSeedAsync({ key: user.pub, seed: inscriptionData.toString() });
const compressedPublicKey = xpubToCompressedPub(derived.key);
const xOnlyPublicKey = utxolib.bitgo.outputScripts.toXOnlyPublicKey(Buffer.from(compressedPublicKey, 'hex'));

Expand Down Expand Up @@ -220,7 +220,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
},
})) as HalfSignedUtxoTransaction;

const derived = this.coin.deriveKeyWithSeed({ key: xprv, seed: inscriptionData.toString() });
const derived = await this.coin.deriveKeyWithSeed({ key: xprv, seed: inscriptionData.toString() });
const prv = xprvToRawPrv(derived.key);

const fullySignedRevealTransaction = await inscriptions.signRevealTransaction(
Expand Down Expand Up @@ -250,7 +250,7 @@ export class InscriptionBuilder implements IInscriptionBuilder {
txPrebuild: PrebuildTransactionResult
): Promise<SubmitTransactionResponse> {
const userKeychain = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] });
const prv = this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });
const prv = await this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });

const halfSigned = (await this.wallet.signTransaction({ prv, txPrebuild })) as HalfSignedUtxoTransaction;
return this.wallet.submitTransaction({ halfSigned });
Expand Down
17 changes: 0 additions & 17 deletions modules/sdk-coin-dot/test/unit/keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,6 @@ describe('Dot KeyPair', () => {
const keyPair = new KeyPair({ pub: bs58Account.publicKey });
should.equal(keyPair.getKeys().pub, publicKeyHexString);
});

it('should be able to derive keypair with hardened derivation', () => {
// using ed25519 (polkadot.js uses sr25519)
const keyPair = new KeyPair({
prv: account1.secretKey,
});
const derivationIndex = 0;
const derived = keyPair.deriveHardened(`m/0'/0'/0'/${derivationIndex}'`);
const derivedKeyPair = new KeyPair({
prv: derived.prv || '',
});
should.exists(derivedKeyPair.getAddress(DotAddressFormat.substrate));
should.exists(derivedKeyPair.getKeys().prv);
should.exists(derivedKeyPair.getKeys().pub);
should.equal(derivedKeyPair.getKeys().prv?.length, 64);
should.equal(derivedKeyPair.getKeys().pub?.length, 64);
});
});

describe('KeyPair validation', () => {
Expand Down
Loading

0 comments on commit 09c61bd

Please sign in to comment.