Skip to content

Commit

Permalink
Add tests for hd keys and code improvements (#159)
Browse files Browse the repository at this point in the history
* tests for hd keys

* rename class static property
  • Loading branch information
0xmaayan authored Nov 2, 2023
1 parent 884122c commit 8a6ef23
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 22 deletions.
6 changes: 3 additions & 3 deletions src/core/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,13 @@ export class Account {
legacy?: boolean;
}): Account {
const { path, mnemonic, scheme, legacy } = args;
let privateKey;
let privateKey: PrivateKey;
switch (scheme) {
case SigningSchemeInput.Secp256k1Ecdsa:
privateKey = new Secp256k1PrivateKey(Secp256k1PrivateKey.fromDerivationPath(path, mnemonic));
privateKey = Secp256k1PrivateKey.fromDerivationPath(path, mnemonic);
break;
case SigningSchemeInput.Ed25519:
privateKey = new Ed25519PrivateKey(Ed25519PrivateKey.fromDerivationPath(path, mnemonic));
privateKey = Ed25519PrivateKey.fromDerivationPath(path, mnemonic);
break;
default:
throw new Error(`Unsupported scheme ${scheme}`);
Expand Down
28 changes: 23 additions & 5 deletions src/core/crypto/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Deserializer } from "../../bcs/deserializer";
import { Serializer } from "../../bcs/serializer";
import { Hex } from "../hex";
import { HexInput } from "../../types";
import { CKDPriv, deriveKey, HARDENED_OFFSET, isValidHardenedPath, KeyType, mnemonicToSeed, splitPath } from "./hdKey";
import { CKDPriv, deriveKey, HARDENED_OFFSET, isValidHardenedPath, mnemonicToSeed, splitPath } from "./hdKey";

/**
* Represents the public key of an Ed25519 key pair.
Expand Down Expand Up @@ -99,6 +99,12 @@ export class Ed25519PrivateKey extends PrivateKey {
*/
static readonly LENGTH: number = 32;

/**
* The Ed25519 key seed to use for BIP-32 compatibility
* See more {@link https://github.com/satoshilabs/slips/blob/master/slip-0010.md}
*/
static readonly SLIP_0010_SEED = "ed25519 seed";

/**
* The Ed25519 signing key
* @private
Expand Down Expand Up @@ -191,13 +197,25 @@ export class Ed25519PrivateKey extends PrivateKey {
*
* @param path the BIP44 path
* @param mnemonics the mnemonic seed phrase
* @param offset the offset used for key derivation, defaults to 0x80000000
*/
static fromDerivationPath(path: string, mnemonics: string, offset = HARDENED_OFFSET): Uint8Array {
static fromDerivationPath(path: string, mnemonics: string): Ed25519PrivateKey {
if (!isValidHardenedPath(path)) {
throw new Error(`Invalid derivation path ${path}`);
}
const { key, chainCode } = deriveKey(KeyType.ED25519, mnemonicToSeed(mnemonics));
return Ed25519PrivateKey.fromDerivationPathInner(path, mnemonicToSeed(mnemonics));
}

/**
* A private inner function so we can separate from the main fromDerivationPath() method
* to add tests to verify we create the keys correctly.
*
* @param path the BIP44 path
* @param seed the seed phrase created by the mnemonics
* @param offset the offset used for key derivation, defaults to 0x80000000
* @returns
*/
private static fromDerivationPathInner(path: string, seed: Uint8Array, offset = HARDENED_OFFSET): Ed25519PrivateKey {
const { key, chainCode } = deriveKey(Ed25519PrivateKey.SLIP_0010_SEED, seed);

const segments = splitPath(path).map((el) => parseInt(el, 10));

Expand All @@ -206,7 +224,7 @@ export class Ed25519PrivateKey extends PrivateKey {
key,
chainCode,
});
return privateKey;
return new Ed25519PrivateKey(privateKey);
}
}

Expand Down
21 changes: 18 additions & 3 deletions src/core/crypto/secp256k1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,19 +181,34 @@ export class Secp256k1PrivateKey extends PrivateKey {
*
* @param path the BIP44 path
* @param mnemonics the mnemonic seed phrase
*
* @returns The generated key
*/
static fromDerivationPath(path: string, mnemonics: string): Uint8Array {
static fromDerivationPath(path: string, mnemonics: string): Secp256k1PrivateKey {
if (!isValidBIP44Path(path)) {
throw new Error(`Invalid derivation path ${path}`);
}
const { privateKey } = HDKey.fromMasterSeed(mnemonicToSeed(mnemonics)).derive(path);
return Secp256k1PrivateKey.fromDerivationPathInner(path, mnemonicToSeed(mnemonics));
}

/**
* A private inner function so we can separate from the main fromDerivationPath() method
* to add tests to verify we create the keys correctly.
*
* @param path the BIP44 path
* @param seed the seed phrase created by the mnemonics
*
* @returns The generated key
*/
private static fromDerivationPathInner(path: string, seed: Uint8Array): Secp256k1PrivateKey {
const { privateKey } = HDKey.fromMasterSeed(seed).derive(path);

// library returns privateKey as Uint8Array | null
if (privateKey === null) {
throw new Error("Invalid key");
}

return privateKey;
return new Secp256k1PrivateKey(privateKey);
}
}

Expand Down
8 changes: 0 additions & 8 deletions tests/unit/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,6 @@ describe("Account", () => {
});
expect(newAccount.accountAddress.toString()).toEqual(address);
});

it("should prevent an invalid bip44 path ", () => {
const { mnemonic } = wallet;
const path = "1234";
expect(() => Account.fromDerivationPath({ path, mnemonic, scheme: SigningSchemeInput.Ed25519 })).toThrow(
"Invalid derivation path",
);
});
});

describe("sign and verify", () => {
Expand Down
15 changes: 14 additions & 1 deletion tests/unit/ed25519.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { Deserializer, Ed25519PrivateKey, Ed25519PublicKey, Ed25519Signature, Hex, Serializer } from "../../src";
import { ed25519 } from "./helper";
import { ed25519, wallet } from "./helper";

describe("Ed25519PublicKey", () => {
it("should create the instance correctly without error", () => {
Expand Down Expand Up @@ -166,6 +166,19 @@ describe("PrivateKey", () => {
expect(publicKey).toBeInstanceOf(Ed25519PublicKey);
expect(publicKey.toString()).toEqual(ed25519.publicKey);
});

it("should prevent an invalid bip44 path ", () => {
const { mnemonic } = wallet;
const path = "1234";
expect(() => Ed25519PrivateKey.fromDerivationPath(path, mnemonic)).toThrow("Invalid derivation path");
});

it("should derive from path and mnemonic", () => {
const { mnemonic, path, privateKey } = wallet;
const key = Ed25519PrivateKey.fromDerivationPath(path, mnemonic);
expect(key).toBeInstanceOf(Ed25519PrivateKey);
expect(privateKey).toEqual(key.toString());
});
});

describe("Signature", () => {
Expand Down
104 changes: 103 additions & 1 deletion tests/unit/hdKey.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { secp256k1WalletTestObject, wallet } from "./helper";
import { isValidBIP44Path, isValidHardenedPath } from "../../src";
import { Ed25519PrivateKey, Hex, isValidBIP44Path, isValidHardenedPath, Secp256k1PrivateKey } from "../../src";

describe("Hierarchical Deterministic Key (hdkey)", () => {
describe("hardened path", () => {
Expand Down Expand Up @@ -61,4 +61,106 @@ describe("Hierarchical Deterministic Key (hdkey)", () => {
expect(isValidBIP44Path("m/44'/637'/0/0/0/")).toBe(false);
});
});

// testing against https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-1-for-ed25519
describe("Ed25519", () => {
const ed25519 = [
{
seed: Hex.fromHexInput("000102030405060708090a0b0c0d0e0f"),
vectors: [
{
chain: "m",
private: "2b4be7f19ee27bbf30c667b642d5f4aa69fd169872f8fc3059c08ebae2eb19e7",
public: "00a4b2856bfec510abab89753fac1ac0e1112364e7d250545963f135f2a33188ed",
},
{
chain: "m/0'",
private: "68e0fe46dfb67e368c75379acec591dad19df3cde26e63b93a8e704f1dade7a3",
public: "008c8a13df77a28f3445213a0f432fde644acaa215fc72dcdf300d5efaa85d350c",
},
{
chain: "m/0'/1'",
private: "b1d0bad404bf35da785a64ca1ac54b2617211d2777696fbffaf208f746ae84f2",
public: "001932a5270f335bed617d5b935c80aedb1a35bd9fc1e31acafd5372c30f5c1187",
},
{
chain: "m/0'/1'/2'",
private: "92a5b23c0b8a99e37d07df3fb9966917f5d06e02ddbd909c7e184371463e9fc9",
public: "00ae98736566d30ed0e9d2f4486a64bc95740d89c7db33f52121f8ea8f76ff0fc1",
},
{
chain: "m/0'/1'/2'/2'",
private: "30d1dc7e5fc04c31219ab25a27ae00b50f6fd66622f6e9c913253d6511d1e662",
public: "008abae2d66361c879b900d204ad2cc4984fa2aa344dd7ddc46007329ac76c429c",
},
{
chain: "m/0'/1'/2'/2'/1000000000'",
private: "8f94d394a8e8fd6b1bc2f3f49f5c47e385281d5c17e65324b0f62483e37e8793",
public: "003c24da049451555d51a7014a37337aa4e12d41e485abccfa46b47dfb2af54b7a",
},
],
},
];

ed25519.forEach(({ seed, vectors }) => {
vectors.forEach(({ chain, private: privateKey }) => {
it(`should generate correct key pair for ${chain}`, () => {
// eslint-disable-next-line @typescript-eslint/dot-notation
const key = Ed25519PrivateKey["fromDerivationPathInner"](chain, seed.toUint8Array());
expect(key.toString()).toBe(`0x${privateKey}`);
});
});
});
});

// testing against https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-1-for-secp256k1
describe("secp256k1", () => {
const secp256k1 = [
{
seed: Hex.fromHexInput("000102030405060708090a0b0c0d0e0f"),
vectors: [
{
chain: "m",
private: "e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35",
public: "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2",
},
{
chain: "m/0'",
private: "edb2e14f9ee77d26dd93b4ecede8d16ed408ce149b6cd80b0715a2d911a0afea",
public: "035a784662a4a20a65bf6aab9ae98a6c068a81c52e4b032c0fb5400c706cfccc56",
},
{
chain: "m/0'/1",
private: "3c6cb8d0f6a264c91ea8b5030fadaa8e538b020f0a387421a12de9319dc93368",
public: "03501e454bf00751f24b1b489aa925215d66af2234e3891c3b21a52bedb3cd711c",
},
{
chain: "m/0'/1/2'",
private: "cbce0d719ecf7431d88e6a89fa1483e02e35092af60c042b1df2ff59fa424dca",
public: "0357bfe1e341d01c69fe5654309956cbea516822fba8a601743a012a7896ee8dc2",
},
{
chain: "m/0'/1/2'/2",
private: "0f479245fb19a38a1954c5c7c0ebab2f9bdfd96a17563ef28a6a4b1a2a764ef4",
public: "02e8445082a72f29b75ca48748a914df60622a609cacfce8ed0e35804560741d29",
},
{
chain: "m/0'/1/2'/2/1000000000",
private: "471b76e389e528d6de6d816857e012c5455051cad6660850e58372a6c3e6e7c8",
public: "022a471424da5e657499d1ff51cb43c47481a03b1e77f951fe64cec9f5a48f7011",
},
],
},
];

secp256k1.forEach(({ seed, vectors }) => {
vectors.forEach(({ chain, private: privateKey }) => {
it(`should generate correct key pair for ${chain}`, () => {
// eslint-disable-next-line @typescript-eslint/dot-notation
const key = Secp256k1PrivateKey["fromDerivationPathInner"](chain, seed.toUint8Array());
expect(key.toString()).toBe(`0x${privateKey}`);
});
});
});
});
});
15 changes: 14 additions & 1 deletion tests/unit/secp256k1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { secp256k1 } from "@noble/curves/secp256k1";
import { secp256k1TestObject } from "./helper";
import { secp256k1TestObject, secp256k1WalletTestObject } from "./helper";
import { Deserializer, Hex, Secp256k1PrivateKey, Secp256k1PublicKey, Secp256k1Signature, Serializer } from "../../src";

/* eslint-disable max-len */
Expand Down Expand Up @@ -127,6 +127,19 @@ describe("Secp256k1PrivateKey", () => {

expect(deserializedPrivateKey.toString()).toEqual(privateKey.toString());
});

it("should prevent an invalid bip44 path ", () => {
const { mnemonic } = secp256k1WalletTestObject;
const path = "1234";
expect(() => Secp256k1PrivateKey.fromDerivationPath(path, mnemonic)).toThrow("Invalid derivation path");
});

it("should derive from path and mnemonic", () => {
const { mnemonic, path, privateKey } = secp256k1WalletTestObject;
const key = Secp256k1PrivateKey.fromDerivationPath(path, mnemonic);
expect(key).toBeInstanceOf(Secp256k1PrivateKey);
expect(privateKey).toEqual(key.toString());
});
});

describe("Secp256k1Signature", () => {
Expand Down

0 comments on commit 8a6ef23

Please sign in to comment.