Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(root): implement Eddsa multisig derivation #4276

Closed
wants to merge 1 commit into from
Closed
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
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:f008e38df59581e2851939f035bebbb18755fcb9d3864eb2aaa22be060ede6:2f5a0abfcd5889100604b0c22a430c4f9e1ac92b43675ca7cef0f9550acd2afa:049aa12f8beaf88eb1a404e4fc5a7ad290accd5b6bb37fa497f2e3bd56fa5990',
pub: 'rpubf008e38df59581e2851939f035bebbb18755fcb9d3864eb2aaa22be060ede6:2f5a0abfcd5889100604b0c22a430c4f9e1ac92b43675ca7cef0f9550acd2afa',
},
},
rootKeys1: {
prv: 'rprvb00584090478b47d1d1bf45ee8bbd012ec3634ec5eb40fb944c22602daac8979:6b0de9d546d71d95a04a9cfe0b6b8cd2f5da4961c6170ff5e8b7d27098e3a909: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, EddsaKeyDeriver.ROOT_PRV_KEY_LENGTH);
assert.ok(rootKeys.prv.startsWith(EddsaKeyDeriver.ROOT_PRV_KEY_PREFIX));
assert.ok(rootKeys.pub);
assert.equal(rootKeys.pub.length, EddsaKeyDeriver.ROOT_PUB_KEY_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
Loading