Skip to content

Commit

Permalink
feat(sdk-coin-algo): implement algo root key creation
Browse files Browse the repository at this point in the history
implemented algo root key creation and deprecated deriverKeyWithSeed

WP-1417

TICKET: WP-1417
  • Loading branch information
alebusse committed Feb 21, 2024
1 parent 69bcaac commit 6541032
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 43 deletions.
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": "^26.0.0",
"@bitgo/statics": "^47.0.0",
"@bitgo/utxo-lib": "^9.34.0",
"@hashgraph/cryptography": "1.1.2",
"@stablelib/hex": "^1.0.0",
"algosdk": "1.14.0",
Expand Down
46 changes: 19 additions & 27 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,7 @@ import {
UnexpectedAddressError,
VerifyAddressOptions,
VerifyTransactionOptions,
NotSupported,
} from '@bitgo/sdk-core';
import stellar from 'stellar-sdk';
import BigNumber from 'bignumber.js';
Expand Down Expand Up @@ -227,12 +226,12 @@ export class Algo extends BaseCoin {
return true;
}

/**
* Generate ed25519 key pair
*
* @param seed
* @returns {Object} object with generated pub, prv
*/
/** inheritdoc */
deriveKeyWithSeed(): { derivationPath: string; key: string } {
throw new NotSupported('method deriveKeyWithSeed not supported for eddsa curve');
}

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

/** inheritdoc */
generateRootKeyPair(seed?: Buffer): KeyPair {
const keyPair = seed ? new AlgoLib.KeyPair({ seed }) : new AlgoLib.KeyPair();
const keys = keyPair.getKeys();
if (!keys.prv) {
throw new Error('Missing prv in key generation.');
}
return { prv: keys.prv + keys.pub, pub: keys.pub };
}

/**
* Return boolean indicating whether input is valid public key for the coin.
*
* @param {String} pub the pub to be checked
* @returns {Boolean} is it valid?
*/
isValidPub(pub: string): boolean {
return AlgoLib.algoUtils.isValidAddress(pub);
return AlgoLib.algoUtils.isValidAddress(pub) || AlgoLib.algoUtils.isValidPublicKey(pub);
}

/**
Expand All @@ -265,7 +274,7 @@ export class Algo extends BaseCoin {
* @returns {Boolean} is it valid?
*/
isValidPrv(prv: string): boolean {
return AlgoLib.algoUtils.isValidSeed(prv);
return AlgoLib.algoUtils.isValidSeed(prv) || AlgoLib.algoUtils.isValidPrivateKey(prv);
}

/**
Expand Down Expand Up @@ -562,23 +571,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
25 changes: 21 additions & 4 deletions modules/sdk-coin-algo/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import {
BaseTransactionBuilder,
BuildTransactionError,
InvalidTransactionError,
isValidEd25519SecretKey,
isValidEd25519Seed,
TransactionType,
} from '@bitgo/sdk-core';
import { algoUtils } from '.';

const MIN_FEE = 1000; // in microalgos

Expand Down Expand Up @@ -325,9 +327,18 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {

/** @inheritdoc */
protected signImplementation({ key }: BaseKey): Transaction {
const buffKey = Utils.decodeSeed(key);
const keypair = new KeyPair({ prv: Buffer.from(buffKey.seed).toString('hex') });
this._keyPairs.push(keypair);
try {
const buffKey = Utils.decodeSeed(key);
const keypair = new KeyPair({ prv: Buffer.from(buffKey.seed).toString('hex') });
this._keyPairs.push(keypair);
} catch (e) {
if (algoUtils.isValidPrivateKey(key)) {
const keypair = new KeyPair({ prv: key });
this._keyPairs.push(keypair);
} else {
throw e;
}
}

return this._transaction;
}
Expand Down Expand Up @@ -371,14 +382,20 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
let isValidPrivateKeyFromBytes;
const isValidPrivateKeyFromHex = isValidEd25519Seed(key);
const isValidPrivateKeyFromBase64 = isValidEd25519Seed(Buffer.from(key, 'base64').toString('hex'));
const isValidRootPrvKey = isValidEd25519SecretKey(key);
try {
const decodedSeed = Utils.decodeSeed(key);
isValidPrivateKeyFromBytes = isValidEd25519Seed(Buffer.from(decodedSeed.seed).toString('hex'));
} catch (err) {
isValidPrivateKeyFromBytes = false;
}

if (!isValidPrivateKeyFromBytes && !isValidPrivateKeyFromHex && !isValidPrivateKeyFromBase64) {
if (
!isValidPrivateKeyFromBytes &&
!isValidPrivateKeyFromHex &&
!isValidPrivateKeyFromBase64 &&
!isValidRootPrvKey
) {
throw new BuildTransactionError(`Key validation failed`);
}
}
Expand Down
14 changes: 14 additions & 0 deletions modules/sdk-coin-algo/test/fixtures/algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,3 +258,17 @@ export const networks = {
genesisHash: 'mFgazF+2uRS1tMiL9dsj01hJGySEmPN28B/TjjvpVW0=',
},
} as const;

export const rootKeyData = {
userKeyPair: {
prv: '7a35bb4007bc7cabad2119a264adf47d9148c484bb70759654d512054d814789de140f20b99f66356e57df3a727c7e5e58f9d5bd58a41c616ac183539b1d211a',
pub: 'de140f20b99f66356e57df3a727c7e5e58f9d5bd58a41c616ac183539b1d211a',
},
backupPub: 'cb64e9729d5a1f1d669a678c8b4751584bd91d71646e27ede39eb92352cc4a34',
bitgoPub: 'JBL247YSR7BD4O3BSPK4NGENAEWKDGFYYMD73MFPJQ4GAW6B4XK4NKDAHU',
unsignedTx:
'iaNhbXTOABFMhKNmZWXNA+iiZnbOAjfvgaNnZW6sdGVzdG5ldC12MS4womdoxCBIY7UYpLPITsgQ8i1PEIHLD3HwWaesIN7GL39w5Qk6IqJsds4CN/Npo3JjdsQgMP3MIxudjDYJr397seXvj5Y6SLByE1KN772IbZauSISjc25kxCBcMEptJu8de1vVNzOWxNZUtvnLrxmObmSa/brOdlWb6KR0eXBlo3BheQ==',
halfSignedTx:
'gqRtc2lng6ZzdWJzaWeTgqJwa8Qg3hQPILmfZjVuV986cnx+Xlj51b1YpBxhasGDU5sdIRqhc8RAUSzKTZBm6OYGZqZ8KtSkIdwEDpOuFZR/O8saJfeC5E77UonmivLF5eQiAlE4+YD285IrXUSGvMgGu6s6wKyzBYGicGvEIMtk6XKdWh8dZppnjItHUVhL2R1xZG4n7eOeuSNSzEo0gaJwa8QgSFeufxKPwj47YZPVxpiNASyhmLjDB/2wr0w4YFvB5dWjdGhyAqF2AaN0eG6Jo2FtdM4AEUyEo2ZlZc0D6KJmds4CN++Bo2dlbqx0ZXN0bmV0LXYxLjCiZ2jEIEhjtRiks8hOyBDyLU8QgcsPcfBZp6wg3sYvf3DlCToiomx2zgI382mjcmN2xCAw/cwjG52MNgmvf3ux5e+PljpIsHITUo3vvYhtlq5IhKNzbmTEIFwwSm0m7x17W9U3M5bE1lS2+cuvGY5uZJr9us52VZvopHR5cGWjcGF5',
senderAddress: 'LQYEU3JG54OXWW6VG4ZZNRGWKS3PTS5PDGHG4ZE27W5M45SVTPUJDZENQA',
} as const;
73 changes: 62 additions & 11 deletions modules/sdk-coin-algo/test/unit/algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Sinon, { SinonStub } from 'sinon';
import assert from 'assert';
import { Algo } from '../../src/algo';
import BigNumber from 'bignumber.js';
import { TransactionBuilderFactory } from '../../src/lib';

describe('ALGO:', function () {
let bitgo: TestBitGoAPI;
Expand Down Expand Up @@ -540,6 +541,20 @@ describe('ALGO:', function () {
signed.txHex.should.equal(AlgoResources.rawTx.transfer.signed);
});

it('should sign transaction with root key', async function () {
const keypair = basecoin.generateRootKeyPair(AlgoResources.accounts.account1.secretKey);

const signed = await basecoin.signTransaction({
txPrebuild: {
txHex: AlgoResources.rawTx.transfer.unsigned,
keys: [keypair.pub],
addressVersion: 1,
},
prv: keypair.prv,
});
signed.txHex.should.equal(AlgoResources.rawTx.transfer.signed);
});

it('should sign half signed transaction', async function () {
const signed = await basecoin.signTransaction({
txPrebuild: {
Expand All @@ -557,6 +572,29 @@ describe('ALGO:', function () {
signed.txHex.should.equal(AlgoResources.rawTx.transfer.multisig);
});

it('should sign half signed transaction with root key', async function () {
const signed = await basecoin.signTransaction({
txPrebuild: {
halfSigned: {
txHex: AlgoResources.rootKeyData.unsignedTx,
},
keys: [
AlgoResources.rootKeyData.userKeyPair.pub,
AlgoResources.rootKeyData.backupPub,
AlgoResources.rootKeyData.bitgoPub,
],
addressVersion: 1,
},
prv: AlgoResources.rootKeyData.userKeyPair.prv,
});

signed.txHex.should.deepEqual(AlgoResources.rootKeyData.halfSignedTx);
const factory = new TransactionBuilderFactory(coins.get('algo'));
const tx = await factory.from(signed.txHex).build();
const txJson = tx.toJson();
txJson.from.should.equal(AlgoResources.rootKeyData.senderAddress);
});

it('should verify sign params if the key array contains addresses', function () {
const keys = [
AlgoResources.accounts.account1.address,
Expand Down Expand Up @@ -624,19 +662,24 @@ describe('ALGO:', function () {
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));
describe('Generate wallet Root key pair: ', () => {
it('should generate key pair', () => {
const kp = basecoin.generateRootKeyPair();
basecoin.isValidPub(kp.pub).should.equal(true);
basecoin.isValidPrv(kp.prv).should.equal(true);
});

basecoin.isValidPub(derivedKeypair.key).should.be.true();
derivedKeypair.should.deepEqual({
key: 'NAYUBR4HQKJBBTNNKXQIY7GMHUCHVAUH5DQ4ZVHVL67CUQLGHRWDKQAHYU',
derivationPath: "m/999999'/25725073'/5121434'",
});
it('should generate key pair from seed', () => {
const seed = Buffer.from('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60', 'hex');
const kp = basecoin.generateRootKeyPair(seed);
basecoin.isValidPub(kp.pub).should.equal(true);
kp.pub.should.equal('d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a');
basecoin.isValidPrv(kp.prv).should.equal(true);
kp.prv.should.equal(
'9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
);
});
});

Expand Down Expand Up @@ -707,6 +750,14 @@ describe('ALGO:', function () {
});
});

describe('deriveKeyWithSeed', function () {
it('should derive key with seed', function () {
(() => {
basecoin.deriveKeyWithSeed('test');
}).should.throw('method deriveKeyWithSeed not supported for eddsa curve');
});
});

describe('Recovery', function () {
const fee = 1000;
const userKey =
Expand Down

0 comments on commit 6541032

Please sign in to comment.