From cc371c75f637df3bccc45018a939aef6ae1211bf Mon Sep 17 00:00:00 2001 From: Jin Date: Fri, 1 Sep 2023 11:16:53 -0700 Subject: [PATCH 1/7] Migrate crypto classes over to sdk v2 --- .../sdk_v2/src/crypto/authentication_key.ts | 73 ++++++++ .../typescript/sdk_v2/src/crypto/ed25519.ts | 51 ++++++ .../sdk_v2/src/crypto/multi_ed25519.ts | 158 ++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts create mode 100644 ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts create mode 100644 ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts diff --git a/ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts b/ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts new file mode 100644 index 0000000000000..510b2104b3699 --- /dev/null +++ b/ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts @@ -0,0 +1,73 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { sha3_256 as sha3Hash } from "@noble/hashes/sha3"; +import { AccountAddress, Hex } from "../core"; +import { HexInput } from "../types"; +import { MultiEd25519PublicKey } from "./multi_ed25519"; +import { Ed25519PublicKey } from "./ed25519"; + +/** + * Each account stores an authentication key. Authentication key enables account owners to rotate + * their private key(s) associated with the account without changing the address that hosts their account. + * @see {@link * https://aptos.dev/concepts/accounts | Account Basics} + * + * Account addresses can be derived from AuthenticationKey + */ +export class AuthenticationKey { + static readonly LENGTH: number = 32; + + static readonly MULTI_ED25519_SCHEME: number = 1; + + static readonly ED25519_SCHEME: number = 0; + + static readonly DERIVE_RESOURCE_ACCOUNT_SCHEME: number = 255; + + readonly data: Hex; + + constructor(hexInput: HexInput) { + if (hexInput.length !== AuthenticationKey.LENGTH) { + throw new Error("Expected a byte array of length 32"); + } + this.data = Hex.fromHexInput({ hexInput }); + } + + /** + * Converts a K-of-N MultiEd25519PublicKey to AuthenticationKey with: + * `auth_key = sha3-256(p_1 | … | p_n | K | 0x01)`. `K` represents the K-of-N required for + * authenticating the transaction. `0x01` is the 1-byte scheme for multisig. + */ + static fromMultiEd25519PublicKey(publicKey: MultiEd25519PublicKey): AuthenticationKey { + const pubKeyBytes = publicKey.toUint8Array(); + + const bytes = new Uint8Array(pubKeyBytes.length + 1); + bytes.set(pubKeyBytes); + bytes.set([AuthenticationKey.MULTI_ED25519_SCHEME], pubKeyBytes.length); + + const hash = sha3Hash.create(); + hash.update(bytes); + + return new AuthenticationKey(hash.digest()); + } + + static fromEd25519PublicKey(publicKey: Ed25519PublicKey): AuthenticationKey { + const pubKeyBytes = publicKey.value.toUint8Array(); + + const bytes = new Uint8Array(pubKeyBytes.length + 1); + bytes.set(pubKeyBytes); + bytes.set([AuthenticationKey.ED25519_SCHEME], pubKeyBytes.length); + + const hash = sha3Hash.create(); + hash.update(bytes); + + return new AuthenticationKey(hash.digest()); + } + + /** + * Derives an account address from AuthenticationKey. Since current AccountAddress is 32 bytes, + * AuthenticationKey bytes are directly translated to AccountAddress. + */ + derivedAddress(): AccountAddress { + return new AccountAddress({ data: this.data.toUint8Array() }); + } +} \ No newline at end of file diff --git a/ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts b/ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts new file mode 100644 index 0000000000000..982141c1c2056 --- /dev/null +++ b/ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts @@ -0,0 +1,51 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Deserializer, Serializer } from "../bcs"; +import { Hex } from "../core"; +import { HexInput } from "../types"; + +export class Ed25519PublicKey { + static readonly LENGTH: number = 32; + + readonly value: Hex; + + constructor(value: HexInput) { + if (value.length !== Ed25519PublicKey.LENGTH) { + throw new Error(`Ed25519PublicKey length should be ${Ed25519PublicKey.LENGTH}`); + } + this.value = Hex.fromHexInput({ hexInput: value }); + } + + toUint8Array(): Uint8Array { + return this.value.toUint8Array(); + } + + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.value.toUint8Array()); + } + + static deserialize(deserializer: Deserializer): Ed25519PublicKey { + const value = deserializer.deserializeBytes(); + return new Ed25519PublicKey(value); + } +} + +export class Ed25519Signature { + static readonly LENGTH = 64; + + constructor(public readonly value: Uint8Array) { + if (value.length !== Ed25519Signature.LENGTH) { + throw new Error(`Ed25519Signature length should be ${Ed25519Signature.LENGTH}`); + } + } + + serialize(serializer: Serializer): void { + serializer.serializeBytes(this.value); + } + + static deserialize(deserializer: Deserializer): Ed25519Signature { + const value = deserializer.deserializeBytes(); + return new Ed25519Signature(value); + } +} \ No newline at end of file diff --git a/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts b/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts new file mode 100644 index 0000000000000..9db94e42d2cb7 --- /dev/null +++ b/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts @@ -0,0 +1,158 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +/* eslint-disable no-bitwise */ +import { Deserializer, Seq, Serializer, Uint8 } from "../bcs"; +import { Ed25519PublicKey, Ed25519Signature } from "./ed25519"; + +/** + * MultiEd25519 currently supports at most 32 signatures. + */ +const MAX_SIGNATURES_SUPPORTED = 32; + +export class MultiEd25519PublicKey { + /** + * Public key for a K-of-N multisig transaction. A K-of-N multisig transaction means that for such a + * transaction to be executed, at least K out of the N authorized signers have signed the transaction + * and passed the check conducted by the chain. + * + * @see {@link + * https://aptos.dev/guides/creating-a-signed-transaction#multisignature-transactions | Creating a Signed Transaction} + * + * @param public_keys A list of public keys + * @param threshold At least "threshold" signatures must be valid + */ + constructor(public readonly public_keys: Seq, public readonly threshold: Uint8) { + if (threshold > MAX_SIGNATURES_SUPPORTED) { + throw new Error(`"threshold" cannot be larger than ${MAX_SIGNATURES_SUPPORTED}`); + } + } + + /** + * Converts a MultiEd25519PublicKey into Uint8Array (bytes) with: bytes = p1_bytes | ... | pn_bytes | threshold + */ + toUint8Array(): Uint8Array { + const bytes = new Uint8Array(this.public_keys.length * Ed25519PublicKey.LENGTH + 1); + this.public_keys.forEach((k: Ed25519PublicKey, i: number) => { + bytes.set(k.value.toUint8Array(), i * Ed25519PublicKey.LENGTH); + }); + + bytes[this.public_keys.length * Ed25519PublicKey.LENGTH] = this.threshold; + + return bytes; + } + + serialize(serializer: Serializer): void { + serializer.serializeByteVector(this.toUint8Array()); + } + + static deserialize(deserializer: Deserializer): MultiEd25519PublicKey { + const bytes = deserializer.deserializeBytes(); + const threshold = bytes[bytes.length - 1]; + + const keys: Seq = []; + + for (let i = 0; i < bytes.length - 1; i += Ed25519PublicKey.LENGTH) { + const begin = i; + keys.push(new Ed25519PublicKey(bytes.subarray(begin, begin + Ed25519PublicKey.LENGTH))); + } + return new MultiEd25519PublicKey(keys, threshold); + } +} + +export class MultiEd25519Signature { + static BITMAP_LEN: Uint8 = 4; + + /** + * Signature for a K-of-N multisig transaction. + * + * @see {@link + * https://aptos.dev/guides/creating-a-signed-transaction#multisignature-transactions | Creating a Signed Transaction} + * + * @param signatures A list of ed25519 signatures + * @param bitmap 4 bytes, at most 32 signatures are supported. If Nth bit value is `1`, the Nth + * signature should be provided in `signatures`. Bits are read from left to right + */ + constructor(public readonly signatures: Seq, public readonly bitmap: Uint8Array) { + if (bitmap.length !== MultiEd25519Signature.BITMAP_LEN) { + throw new Error(`"bitmap" length should be ${MultiEd25519Signature.BITMAP_LEN}`); + } + } + + /** + * Converts a MultiEd25519Signature into Uint8Array (bytes) with `bytes = s1_bytes | ... | sn_bytes | bitmap` + */ + toUint8Array(): Uint8Array { + const bytes = new Uint8Array(this.signatures.length * Ed25519Signature.LENGTH + MultiEd25519Signature.BITMAP_LEN); + this.signatures.forEach((k: Ed25519Signature, i: number) => { + bytes.set(k.value, i * Ed25519Signature.LENGTH); + }); + + bytes.set(this.bitmap, this.signatures.length * Ed25519Signature.LENGTH); + + return bytes; + } + + /** + * Helper method to create a bitmap out of the specified bit positions + * @param bits The bitmap positions that should be set. A position starts at index 0. + * Valid position should range between 0 and 31. + * @example + * Here's an example of valid `bits` + * ``` + * [0, 2, 31] + * ``` + * `[0, 2, 31]` means the 1st, 3rd and 32nd bits should be set in the bitmap. + * The result bitmap should be 0b1010000000000000000000000000001 + * + * @returns bitmap that is 32bit long + */ + static createBitmap(bits: Seq): Uint8Array { + // Bits are read from left to right. e.g. 0b10000000 represents the first bit is set in one byte. + // The decimal value of 0b10000000 is 128. + const firstBitInByte = 128; + const bitmap = new Uint8Array([0, 0, 0, 0]); + + // Check if duplicates exist in bits + const dupCheckSet = new Set(); + + bits.forEach((bit: number) => { + if (bit >= MAX_SIGNATURES_SUPPORTED) { + throw new Error(`Invalid bit value ${bit}.`); + } + + if (dupCheckSet.has(bit)) { + throw new Error("Duplicated bits detected."); + } + + dupCheckSet.add(bit); + + const byteOffset = Math.floor(bit / 8); + + let byte = bitmap[byteOffset]; + + byte |= firstBitInByte >> bit % 8; + + bitmap[byteOffset] = byte; + }); + + return bitmap; + } + + serialize(serializer: Serializer): void { + serializer.serializeByteVector(this.toUint8Array()); + } + + static deserialize(deserializer: Deserializer): MultiEd25519Signature { + const bytes = deserializer.deserializeBytes(); + const bitmap = bytes.subarray(bytes.length - 4); + + const sigs: Seq = []; + + for (let i = 0; i < bytes.length - bitmap.length; i += Ed25519Signature.LENGTH) { + const begin = i; + sigs.push(new Ed25519Signature(bytes.subarray(begin, begin + Ed25519Signature.LENGTH))); + } + return new MultiEd25519Signature(sigs, bitmap); + } +} \ No newline at end of file From d9d7aca75f8cd74d79db084ec101f8608df68d56 Mon Sep 17 00:00:00 2001 From: Jin Date: Mon, 11 Sep 2023 15:01:31 -0700 Subject: [PATCH 2/7] remove Seq --- .../sdk_v2/src/crypto/multi_ed25519.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts b/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts index 9db94e42d2cb7..29f3a98555a7d 100644 --- a/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts +++ b/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /* eslint-disable no-bitwise */ -import { Deserializer, Seq, Serializer, Uint8 } from "../bcs"; +import { Deserializer, Serializer, Uint8 } from "../bcs"; import { Ed25519PublicKey, Ed25519Signature } from "./ed25519"; /** @@ -22,7 +22,7 @@ export class MultiEd25519PublicKey { * @param public_keys A list of public keys * @param threshold At least "threshold" signatures must be valid */ - constructor(public readonly public_keys: Seq, public readonly threshold: Uint8) { + constructor(public readonly public_keys: Ed25519PublicKey[], public readonly threshold: Uint8) { if (threshold > MAX_SIGNATURES_SUPPORTED) { throw new Error(`"threshold" cannot be larger than ${MAX_SIGNATURES_SUPPORTED}`); } @@ -43,14 +43,14 @@ export class MultiEd25519PublicKey { } serialize(serializer: Serializer): void { - serializer.serializeByteVector(this.toUint8Array()); + serializer.serializeBytes(this.toUint8Array()); } static deserialize(deserializer: Deserializer): MultiEd25519PublicKey { const bytes = deserializer.deserializeBytes(); const threshold = bytes[bytes.length - 1]; - const keys: Seq = []; + const keys: Ed25519PublicKey[] = []; for (let i = 0; i < bytes.length - 1; i += Ed25519PublicKey.LENGTH) { const begin = i; @@ -73,7 +73,7 @@ export class MultiEd25519Signature { * @param bitmap 4 bytes, at most 32 signatures are supported. If Nth bit value is `1`, the Nth * signature should be provided in `signatures`. Bits are read from left to right */ - constructor(public readonly signatures: Seq, public readonly bitmap: Uint8Array) { + constructor(public readonly signatures: Ed25519Signature[], public readonly bitmap: Uint8Array) { if (bitmap.length !== MultiEd25519Signature.BITMAP_LEN) { throw new Error(`"bitmap" length should be ${MultiEd25519Signature.BITMAP_LEN}`); } @@ -107,7 +107,7 @@ export class MultiEd25519Signature { * * @returns bitmap that is 32bit long */ - static createBitmap(bits: Seq): Uint8Array { + static createBitmap(bits: Uint8[]): Uint8Array { // Bits are read from left to right. e.g. 0b10000000 represents the first bit is set in one byte. // The decimal value of 0b10000000 is 128. const firstBitInByte = 128; @@ -140,14 +140,14 @@ export class MultiEd25519Signature { } serialize(serializer: Serializer): void { - serializer.serializeByteVector(this.toUint8Array()); + serializer.serializeBytes(this.toUint8Array()); } static deserialize(deserializer: Deserializer): MultiEd25519Signature { const bytes = deserializer.deserializeBytes(); const bitmap = bytes.subarray(bytes.length - 4); - const sigs: Seq = []; + const sigs: Ed25519Signature[] = []; for (let i = 0; i < bytes.length - bitmap.length; i += Ed25519Signature.LENGTH) { const begin = i; From 2a3fa0752a309dde89574be36b07b3f69dfe89bd Mon Sep 17 00:00:00 2001 From: Jin Date: Tue, 12 Sep 2023 16:22:36 -0700 Subject: [PATCH 3/7] Fix from comment and added authentication_key.test.ts --- .../sdk_v2/src/crypto/authentication_key.ts | 7 +-- .../typescript/sdk_v2/src/crypto/ed25519.ts | 7 +-- .../tests/unit/authentication_key.test.ts | 45 +++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 ecosystem/typescript/sdk_v2/tests/unit/authentication_key.test.ts diff --git a/ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts b/ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts index 510b2104b3699..646250bc5f913 100644 --- a/ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts +++ b/ecosystem/typescript/sdk_v2/src/crypto/authentication_key.ts @@ -26,10 +26,11 @@ export class AuthenticationKey { readonly data: Hex; constructor(hexInput: HexInput) { - if (hexInput.length !== AuthenticationKey.LENGTH) { - throw new Error("Expected a byte array of length 32"); + const hex = Hex.fromHexInput({ hexInput }); + if (hex.toUint8Array().length !== AuthenticationKey.LENGTH) { + throw new Error("Expected a hexinput of length 32"); } - this.data = Hex.fromHexInput({ hexInput }); + this.data = hex; } /** diff --git a/ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts b/ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts index 982141c1c2056..18f198b93946e 100644 --- a/ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts +++ b/ecosystem/typescript/sdk_v2/src/crypto/ed25519.ts @@ -10,11 +10,12 @@ export class Ed25519PublicKey { readonly value: Hex; - constructor(value: HexInput) { - if (value.length !== Ed25519PublicKey.LENGTH) { + constructor(hexInput: HexInput) { + const hex = Hex.fromHexInput({ hexInput }); + if (hex.toUint8Array().length !== Ed25519PublicKey.LENGTH) { throw new Error(`Ed25519PublicKey length should be ${Ed25519PublicKey.LENGTH}`); } - this.value = Hex.fromHexInput({ hexInput: value }); + this.value = hex; } toUint8Array(): Uint8Array { diff --git a/ecosystem/typescript/sdk_v2/tests/unit/authentication_key.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/authentication_key.test.ts new file mode 100644 index 0000000000000..a0a9a80c698d7 --- /dev/null +++ b/ecosystem/typescript/sdk_v2/tests/unit/authentication_key.test.ts @@ -0,0 +1,45 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { AuthenticationKey } from "../../src/crypto/authentication_key"; +import { Ed25519PublicKey } from "../../src/crypto/ed25519"; +import { MultiEd25519PublicKey } from "../../src/crypto/multi_ed25519"; + +// Auth key and Public key pair for testing +const auth_key_hexInput = '0x6324287105756b0338e0f84025bd0ac80e58154eb94257b0d4f06ec6497e656e'; +const public_key = '0x719ab6a6d406931ca80efa922e3377390a8d2803e42ecdbf394e979f9a5e57bc'; + +describe('AuthenticationKey', () => { + it('should create an instance with save the hexinput correctly', () => { + const authKey = new AuthenticationKey(auth_key_hexInput); + expect(authKey).toBeInstanceOf(AuthenticationKey); + expect(authKey.data.toString()).toEqual(auth_key_hexInput); + }); + + it('should throw an error with invalid hex input length', () => { + const invalidHexInput = '0123456789abcdef'; // Invalid length + expect(() => new AuthenticationKey(invalidHexInput)).toThrowError( + 'Expected a hexinput of length 32' + ); + }); + + it('should create AuthenticationKey from Ed25519PublicKey', () => { + const publicKey = new Ed25519PublicKey(public_key); + const authKey = AuthenticationKey.fromEd25519PublicKey(publicKey); + expect(authKey).toBeInstanceOf(AuthenticationKey); + expect(authKey.data.toString()).toEqual(auth_key_hexInput); + }); + + it('should create AuthenticationKey from MultiEd25519PublicKey', () => { + const publicKey = new MultiEd25519PublicKey([new Ed25519PublicKey(public_key)], 1); + const authKey = AuthenticationKey.fromMultiEd25519PublicKey(publicKey); + expect(authKey).toBeInstanceOf(AuthenticationKey); + expect(authKey.data.toString()).toEqual('0xd0cb1ed17413857dd59f4ee948b6678c339ad6d5cc97246f65825439f53fd944'); + }); + + it('should derive an AccountAddress from AuthenticationKey with same string', () => { + const authKey = new AuthenticationKey(auth_key_hexInput); + const accountAddress = authKey.derivedAddress(); + expect(accountAddress.toString()).toEqual(auth_key_hexInput) + }); +}); \ No newline at end of file From 853ab467583ed47022ba6e3a0adf48f05e4b642f Mon Sep 17 00:00:00 2001 From: Jin Date: Wed, 13 Sep 2023 13:48:38 -0700 Subject: [PATCH 4/7] add ed25519 test and multi ed25519 --- .../sdk_v2/tests/unit/ed25519.test.ts | 18 +++ .../sdk_v2/tests/unit/multi_ed25519.test.ts | 110 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 ecosystem/typescript/sdk_v2/tests/unit/ed25519.test.ts create mode 100644 ecosystem/typescript/sdk_v2/tests/unit/multi_ed25519.test.ts diff --git a/ecosystem/typescript/sdk_v2/tests/unit/ed25519.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/ed25519.test.ts new file mode 100644 index 0000000000000..878d51e105fd2 --- /dev/null +++ b/ecosystem/typescript/sdk_v2/tests/unit/ed25519.test.ts @@ -0,0 +1,18 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Hex } from "../../src/core/hex"; +import { Ed25519PublicKey, Ed25519Signature } from "../../src/crypto/ed25519"; + +describe("Ed25519", () => { + it("public key serializes to bytes correctly", async () => { + const publicKey = "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200"; + const ed25519PublicKey = new Ed25519PublicKey(publicKey); + + expect(Hex.fromHexInput({ hexInput: ed25519PublicKey.toUint8Array() }).toStringWithoutPrefix()).toEqual( + "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200", + ); + }); + + // TODO: Add test for deserializing +}); diff --git a/ecosystem/typescript/sdk_v2/tests/unit/multi_ed25519.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/multi_ed25519.test.ts new file mode 100644 index 0000000000000..332a5cfa76f4a --- /dev/null +++ b/ecosystem/typescript/sdk_v2/tests/unit/multi_ed25519.test.ts @@ -0,0 +1,110 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Deserializer } from "../../src/bcs/deserializer"; +import { Hex } from "../../src/core/hex"; +import { Ed25519PublicKey, Ed25519Signature } from "../../src/crypto/ed25519"; +import { MultiEd25519PublicKey, MultiEd25519Signature } from "../../src/crypto/multi_ed25519"; + +describe("MultiEd25519", () => { + it("public key serializes to bytes correctly", async () => { + const publicKey1 = "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200"; + const publicKey2 = "aef3f4a4b8eca1dfc343361bf8e436bd42de9259c04b8314eb8e2054dd6e82ab"; + const publicKey3 = "8a5762e21ac1cdb3870442c77b4c3af58c7cedb8779d0270e6d4f1e2f7367d74"; + + const pubKeyMultiSig = new MultiEd25519PublicKey( + [ + new Ed25519PublicKey(publicKey1), + new Ed25519PublicKey(publicKey2), + new Ed25519PublicKey(publicKey3), + ], + 2, + ); + + expect(pubKeyMultiSig.toUint8Array()).toEqual( + "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200aef3f4a4b8eca1dfc343361bf8e436bd42de9259c04b8314eb8e2054dd6e82ab8a5762e21ac1cdb3870442c77b4c3af58c7cedb8779d0270e6d4f1e2f7367d7402", + ); + }); + + it("public key deserializes from bytes correctly", async () => { + const publicKey1 = "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200"; + const publicKey2 = "aef3f4a4b8eca1dfc343361bf8e436bd42de9259c04b8314eb8e2054dd6e82ab"; + const publicKey3 = "8a5762e21ac1cdb3870442c77b4c3af58c7cedb8779d0270e6d4f1e2f7367d74"; + + const pubKeyMultiSig = new MultiEd25519PublicKey( + [ + new Ed25519PublicKey(publicKey1), + new Ed25519PublicKey(publicKey2), + new Ed25519PublicKey(publicKey3), + ], + 2, + ); + const deserialzed = MultiEd25519PublicKey.deserialize(new Deserializer(bcsToBytes(pubKeyMultiSig))); + expect(new Hex({ data: deserialzed.toUint8Array() })).toEqual( + new Hex({ data: pubKeyMultiSig.toUint8Array() }) + ); + }); + + it("signature serializes to bytes correctly", async () => { + // eslint-disable-next-line operator-linebreak + const sig1 = + "e6f3ba05469b2388492397840183945d4291f0dd3989150de3248e06b4cefe0ddf6180a80a0f04c045ee8f362870cb46918478cd9b56c66076f94f3efd5a8805"; + // eslint-disable-next-line operator-linebreak + const sig2 = + "2ae0818b7e51b853f1e43dc4c89a1f5fabc9cb256030a908f9872f3eaeb048fb1e2b4ffd5a9d5d1caedd0c8b7d6155ed8071e913536fa5c5a64327b6f2d9a102"; + const bitmap = "c0000000"; + + const multisig = new MultiEd25519Signature( + [ + new Ed25519Signature(Hex.fromString({ str: sig1}).toUint8Array()), + new Ed25519Signature(Hex.fromString({ str: sig2}).toUint8Array()), + ], + Hex.fromString({ str: bitmap}).toUint8Array(), + ); + + expect(Hex.fromHexInput({ hexInput: multisig.toUint8Array()}).toStringWithoutPrefix()).toEqual( + "e6f3ba05469b2388492397840183945d4291f0dd3989150de3248e06b4cefe0ddf6180a80a0f04c045ee8f362870cb46918478cd9b56c66076f94f3efd5a88052ae0818b7e51b853f1e43dc4c89a1f5fabc9cb256030a908f9872f3eaeb048fb1e2b4ffd5a9d5d1caedd0c8b7d6155ed8071e913536fa5c5a64327b6f2d9a102c0000000", + ); + }); + + it("signature deserializes from bytes correctly", async () => { + // eslint-disable-next-line operator-linebreak + const sig1 = + "e6f3ba05469b2388492397840183945d4291f0dd3989150de3248e06b4cefe0ddf6180a80a0f04c045ee8f362870cb46918478cd9b56c66076f94f3efd5a8805"; + // eslint-disable-next-line operator-linebreak + const sig2 = + "2ae0818b7e51b853f1e43dc4c89a1f5fabc9cb256030a908f9872f3eaeb048fb1e2b4ffd5a9d5d1caedd0c8b7d6155ed8071e913536fa5c5a64327b6f2d9a102"; + const bitmap = "c0000000"; + + const multisig = new MultiEd25519Signature( + [ + new Ed25519Signature(Hex.fromString({ str: sig1}).toUint8Array()), + new Ed25519Signature(Hex.fromString({ str: sig2}).toUint8Array()), + ], + Hex.fromString({ str: bitmap}).toUint8Array(), + ); + + const deserialzed = MultiEd25519Signature.deserialize(new Deserializer(bcsToBytes(multisig))); + expect(Hex.fromHexInput({ hexInput: deserialzed.toUint8Array() })).toEqual( + Hex.fromHexInput({ hexInput: multisig.toUint8Array() }), + ); + }); + + it("creates a valid bitmap", () => { + expect(MultiEd25519Signature.createBitmap([0, 2, 31])).toEqual( + new Uint8Array([0b10100000, 0b00000000, 0b00000000, 0b00000001]), + ); + }); + + it("throws exception when creating a bitmap with wrong bits", async () => { + expect(() => { + MultiEd25519Signature.createBitmap([32]); + }).toThrow("Invalid bit value 32."); + }); + + it("throws exception when creating a bitmap with duplicate bits", async () => { + expect(() => { + MultiEd25519Signature.createBitmap([2, 2]); + }).toThrow("Duplicated bits detected."); + }); +}); From 83a7ecb27c75fbbbeed7585fed6cabd87e7fb87f Mon Sep 17 00:00:00 2001 From: Jin Date: Thu, 14 Sep 2023 15:36:15 -0700 Subject: [PATCH 5/7] update unit tests --- .../sdk_v2/src/crypto/multi_ed25519.ts | 3 +- .../tests/unit/authentication_key.test.ts | 28 +++++---- .../sdk_v2/tests/unit/ed25519.test.ts | 58 ++++++++++++++++--- .../sdk_v2/tests/unit/multi_ed25519.test.ts | 41 ++++++------- 4 files changed, 84 insertions(+), 46 deletions(-) diff --git a/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts b/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts index 29f3a98555a7d..7726634cec8d8 100644 --- a/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts +++ b/ecosystem/typescript/sdk_v2/src/crypto/multi_ed25519.ts @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 /* eslint-disable no-bitwise */ -import { Deserializer, Serializer, Uint8 } from "../bcs"; +import { Deserializer, Serializer } from "../bcs"; +import { Uint8 } from "../types"; import { Ed25519PublicKey, Ed25519Signature } from "./ed25519"; /** diff --git a/ecosystem/typescript/sdk_v2/tests/unit/authentication_key.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/authentication_key.test.ts index a0a9a80c698d7..48996eb63c23f 100644 --- a/ecosystem/typescript/sdk_v2/tests/unit/authentication_key.test.ts +++ b/ecosystem/typescript/sdk_v2/tests/unit/authentication_key.test.ts @@ -6,40 +6,38 @@ import { Ed25519PublicKey } from "../../src/crypto/ed25519"; import { MultiEd25519PublicKey } from "../../src/crypto/multi_ed25519"; // Auth key and Public key pair for testing -const auth_key_hexInput = '0x6324287105756b0338e0f84025bd0ac80e58154eb94257b0d4f06ec6497e656e'; -const public_key = '0x719ab6a6d406931ca80efa922e3377390a8d2803e42ecdbf394e979f9a5e57bc'; +const auth_key_hexInput = "0x6324287105756b0338e0f84025bd0ac80e58154eb94257b0d4f06ec6497e656e"; +const public_key = "0x719ab6a6d406931ca80efa922e3377390a8d2803e42ecdbf394e979f9a5e57bc"; -describe('AuthenticationKey', () => { - it('should create an instance with save the hexinput correctly', () => { +describe("AuthenticationKey", () => { + it("should create an instance with save the hexinput correctly", () => { const authKey = new AuthenticationKey(auth_key_hexInput); expect(authKey).toBeInstanceOf(AuthenticationKey); expect(authKey.data.toString()).toEqual(auth_key_hexInput); }); - it('should throw an error with invalid hex input length', () => { - const invalidHexInput = '0123456789abcdef'; // Invalid length - expect(() => new AuthenticationKey(invalidHexInput)).toThrowError( - 'Expected a hexinput of length 32' - ); + it("should throw an error with invalid hex input length", () => { + const invalidHexInput = "0123456789abcdef"; // Invalid length + expect(() => new AuthenticationKey(invalidHexInput)).toThrowError("Expected a hexinput of length 32"); }); - it('should create AuthenticationKey from Ed25519PublicKey', () => { + it("should create AuthenticationKey from Ed25519PublicKey", () => { const publicKey = new Ed25519PublicKey(public_key); const authKey = AuthenticationKey.fromEd25519PublicKey(publicKey); expect(authKey).toBeInstanceOf(AuthenticationKey); expect(authKey.data.toString()).toEqual(auth_key_hexInput); }); - it('should create AuthenticationKey from MultiEd25519PublicKey', () => { + it("should create AuthenticationKey from MultiEd25519PublicKey", () => { const publicKey = new MultiEd25519PublicKey([new Ed25519PublicKey(public_key)], 1); const authKey = AuthenticationKey.fromMultiEd25519PublicKey(publicKey); expect(authKey).toBeInstanceOf(AuthenticationKey); - expect(authKey.data.toString()).toEqual('0xd0cb1ed17413857dd59f4ee948b6678c339ad6d5cc97246f65825439f53fd944'); + expect(authKey.data.toString()).toEqual("0xd0cb1ed17413857dd59f4ee948b6678c339ad6d5cc97246f65825439f53fd944"); }); - it('should derive an AccountAddress from AuthenticationKey with same string', () => { + it("should derive an AccountAddress from AuthenticationKey with same string", () => { const authKey = new AuthenticationKey(auth_key_hexInput); const accountAddress = authKey.derivedAddress(); - expect(accountAddress.toString()).toEqual(auth_key_hexInput) + expect(accountAddress.toString()).toEqual(auth_key_hexInput); }); -}); \ No newline at end of file +}); diff --git a/ecosystem/typescript/sdk_v2/tests/unit/ed25519.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/ed25519.test.ts index 878d51e105fd2..cde48f0f615a6 100644 --- a/ecosystem/typescript/sdk_v2/tests/unit/ed25519.test.ts +++ b/ecosystem/typescript/sdk_v2/tests/unit/ed25519.test.ts @@ -1,18 +1,62 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 +import { Deserializer } from "../../src/bcs/deserializer"; +import { Serializer } from "../../src/bcs/serializer"; import { Hex } from "../../src/core/hex"; import { Ed25519PublicKey, Ed25519Signature } from "../../src/crypto/ed25519"; -describe("Ed25519", () => { - it("public key serializes to bytes correctly", async () => { - const publicKey = "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200"; - const ed25519PublicKey = new Ed25519PublicKey(publicKey); +describe("Ed25519PublicKey", () => { + it("should create instance correctly without error", () => { + const hexInput = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + const publicKey = new Ed25519PublicKey(hexInput); + expect(publicKey).toBeInstanceOf(Ed25519PublicKey); + }); + + it("should throw an error with invalid hex input length", () => { + const invalidHexInput = "0123456789abcdef"; // Invalid length + expect(() => new Ed25519PublicKey(invalidHexInput)).toThrowError( + `Ed25519PublicKey length should be ${Ed25519PublicKey.LENGTH}`, + ); + }); + + it("should serialize and deserialize correctly", () => { + const hexInput = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + const publicKey = new Ed25519PublicKey(hexInput); + const serializer = new Serializer(); + publicKey.serialize(serializer); + + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserializedPublicKey = Ed25519PublicKey.deserialize(deserializer); + + expect(deserializedPublicKey).toEqual(publicKey); + }); +}); + +describe("Ed25519Signature", () => { + it("should create an instance correctly without error", () => { + const signatureValue = new Uint8Array(Ed25519Signature.LENGTH); + const signature = new Ed25519Signature(signatureValue); + expect(signature).toBeInstanceOf(Ed25519Signature); + }); - expect(Hex.fromHexInput({ hexInput: ed25519PublicKey.toUint8Array() }).toStringWithoutPrefix()).toEqual( - "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200", + it("should throw an error with invalid value length", () => { + const invalidSignatureValue = new Uint8Array(Ed25519Signature.LENGTH - 1); // Invalid length + expect(() => new Ed25519Signature(invalidSignatureValue)).toThrowError( + `Ed25519Signature length should be ${Ed25519Signature.LENGTH}`, ); }); - // TODO: Add test for deserializing + it("should serialize and deserialize correctly", () => { + const signatureValue = new Uint8Array(Ed25519Signature.LENGTH); + // Initialize the signatureValue with some data if needed + const signature = new Ed25519Signature(signatureValue); + const serializer = new Serializer(); + signature.serialize(serializer); + + const deserializer = new Deserializer(serializer.toUint8Array()); + const deserializedSignature = Ed25519Signature.deserialize(deserializer); + + expect(deserializedSignature).toEqual(signature); + }); }); diff --git a/ecosystem/typescript/sdk_v2/tests/unit/multi_ed25519.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/multi_ed25519.test.ts index 332a5cfa76f4a..b74eb620d3c29 100644 --- a/ecosystem/typescript/sdk_v2/tests/unit/multi_ed25519.test.ts +++ b/ecosystem/typescript/sdk_v2/tests/unit/multi_ed25519.test.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Deserializer } from "../../src/bcs/deserializer"; +import { Serializer } from "../../src/bcs/serializer"; import { Hex } from "../../src/core/hex"; import { Ed25519PublicKey, Ed25519Signature } from "../../src/crypto/ed25519"; import { MultiEd25519PublicKey, MultiEd25519Signature } from "../../src/crypto/multi_ed25519"; @@ -13,15 +14,11 @@ describe("MultiEd25519", () => { const publicKey3 = "8a5762e21ac1cdb3870442c77b4c3af58c7cedb8779d0270e6d4f1e2f7367d74"; const pubKeyMultiSig = new MultiEd25519PublicKey( - [ - new Ed25519PublicKey(publicKey1), - new Ed25519PublicKey(publicKey2), - new Ed25519PublicKey(publicKey3), - ], + [new Ed25519PublicKey(publicKey1), new Ed25519PublicKey(publicKey2), new Ed25519PublicKey(publicKey3)], 2, ); - expect(pubKeyMultiSig.toUint8Array()).toEqual( + expect(Hex.fromHexInput({ hexInput: pubKeyMultiSig.toUint8Array() }).toStringWithoutPrefix()).toEqual( "b9c6ee1630ef3e711144a648db06bbb2284f7274cfbee53ffcee503cc1a49200aef3f4a4b8eca1dfc343361bf8e436bd42de9259c04b8314eb8e2054dd6e82ab8a5762e21ac1cdb3870442c77b4c3af58c7cedb8779d0270e6d4f1e2f7367d7402", ); }); @@ -32,17 +29,13 @@ describe("MultiEd25519", () => { const publicKey3 = "8a5762e21ac1cdb3870442c77b4c3af58c7cedb8779d0270e6d4f1e2f7367d74"; const pubKeyMultiSig = new MultiEd25519PublicKey( - [ - new Ed25519PublicKey(publicKey1), - new Ed25519PublicKey(publicKey2), - new Ed25519PublicKey(publicKey3), - ], + [new Ed25519PublicKey(publicKey1), new Ed25519PublicKey(publicKey2), new Ed25519PublicKey(publicKey3)], 2, ); - const deserialzed = MultiEd25519PublicKey.deserialize(new Deserializer(bcsToBytes(pubKeyMultiSig))); - expect(new Hex({ data: deserialzed.toUint8Array() })).toEqual( - new Hex({ data: pubKeyMultiSig.toUint8Array() }) - ); + const serializer = new Serializer(); + serializer.serialize(pubKeyMultiSig); + const deserialzed = MultiEd25519PublicKey.deserialize(new Deserializer(serializer.toUint8Array())); + expect(new Hex({ data: deserialzed.toUint8Array() })).toEqual(new Hex({ data: pubKeyMultiSig.toUint8Array() })); }); it("signature serializes to bytes correctly", async () => { @@ -56,13 +49,13 @@ describe("MultiEd25519", () => { const multisig = new MultiEd25519Signature( [ - new Ed25519Signature(Hex.fromString({ str: sig1}).toUint8Array()), - new Ed25519Signature(Hex.fromString({ str: sig2}).toUint8Array()), + new Ed25519Signature(Hex.fromString({ str: sig1 }).toUint8Array()), + new Ed25519Signature(Hex.fromString({ str: sig2 }).toUint8Array()), ], - Hex.fromString({ str: bitmap}).toUint8Array(), + Hex.fromString({ str: bitmap }).toUint8Array(), ); - expect(Hex.fromHexInput({ hexInput: multisig.toUint8Array()}).toStringWithoutPrefix()).toEqual( + expect(Hex.fromHexInput({ hexInput: multisig.toUint8Array() }).toStringWithoutPrefix()).toEqual( "e6f3ba05469b2388492397840183945d4291f0dd3989150de3248e06b4cefe0ddf6180a80a0f04c045ee8f362870cb46918478cd9b56c66076f94f3efd5a88052ae0818b7e51b853f1e43dc4c89a1f5fabc9cb256030a908f9872f3eaeb048fb1e2b4ffd5a9d5d1caedd0c8b7d6155ed8071e913536fa5c5a64327b6f2d9a102c0000000", ); }); @@ -78,13 +71,15 @@ describe("MultiEd25519", () => { const multisig = new MultiEd25519Signature( [ - new Ed25519Signature(Hex.fromString({ str: sig1}).toUint8Array()), - new Ed25519Signature(Hex.fromString({ str: sig2}).toUint8Array()), + new Ed25519Signature(Hex.fromString({ str: sig1 }).toUint8Array()), + new Ed25519Signature(Hex.fromString({ str: sig2 }).toUint8Array()), ], - Hex.fromString({ str: bitmap}).toUint8Array(), + Hex.fromString({ str: bitmap }).toUint8Array(), ); - const deserialzed = MultiEd25519Signature.deserialize(new Deserializer(bcsToBytes(multisig))); + const serializer = new Serializer(); + serializer.serialize(multisig); + const deserialzed = MultiEd25519Signature.deserialize(new Deserializer(serializer.toUint8Array())); expect(Hex.fromHexInput({ hexInput: deserialzed.toUint8Array() })).toEqual( Hex.fromHexInput({ hexInput: multisig.toUint8Array() }), ); From 109d06b4492af59d4c60383e911e1fe2ff666218 Mon Sep 17 00:00:00 2001 From: Jin Date: Mon, 11 Sep 2023 15:59:05 -0700 Subject: [PATCH 6/7] Add account class for sdk v2 --- .../typescript/sdk_v2/src/core/account.ts | 159 ++++++++++++++++++ ecosystem/typescript/sdk_v2/src/core/index.ts | 1 + .../typescript/sdk_v2/src/utils/hd-key.ts | 76 +++++++++ .../typescript/sdk_v2/src/utils/index.ts | 2 + .../sdk_v2/src/utils/memoize-decorator.ts | 151 +++++++++++++++++ 5 files changed, 389 insertions(+) create mode 100644 ecosystem/typescript/sdk_v2/src/core/account.ts create mode 100644 ecosystem/typescript/sdk_v2/src/utils/hd-key.ts create mode 100644 ecosystem/typescript/sdk_v2/src/utils/index.ts create mode 100644 ecosystem/typescript/sdk_v2/src/utils/memoize-decorator.ts diff --git a/ecosystem/typescript/sdk_v2/src/core/account.ts b/ecosystem/typescript/sdk_v2/src/core/account.ts new file mode 100644 index 0000000000000..61de0a503f765 --- /dev/null +++ b/ecosystem/typescript/sdk_v2/src/core/account.ts @@ -0,0 +1,159 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import nacl from "tweetnacl"; +import * as bip39 from "@scure/bip39"; +import { AccountAddress } from "./account_address"; +import { Memoize, derivePath } from "../utils"; +import { Hex } from "./hex"; +import { bytesToHex } from "@noble/hashes/utils"; +import { Ed25519PublicKey } from "../crypto/ed25519"; +import { AuthenticationKey } from "../crypto/authentication_key"; + +/** + * Class for creating and managing account on Aptos network + * + * Use this class to create accounts, sign transactions, and more. + */ +export class Account { + /** + * signing key of the account, which holds the public and private key + */ + private readonly _signingKey: nacl.SignKeyPair; + + /** + * Account address associated with the account + */ + private readonly _accountAddress: AccountAddress; + + /** + * Public key of the account + * + * @returns Hex - public key of the account + */ + get publicKey(): Hex { + return new Hex({ data: this._signingKey.publicKey }); + } + + /** + * Private key of the account + * + * @returns Hex - private key of the account + */ + get privateKey(): Hex { + return new Hex({ data: this._signingKey.secretKey }); + } + + /** + * Address of the account + * + * @returns AccountAddress - address of the account + */ + get accountAddress(): AccountAddress { + return this._accountAddress; + } + + /** + * private constructor for Account + * + * This method is private because it should only be called by the factory static methods. + * @returns Account + */ + private constructor(keyPair: nacl.SignKeyPair, address: AccountAddress) { + this._signingKey = keyPair; + this._accountAddress = address; + } + + /** + * Creates new account with random private key and address + * + * @returns Account + */ + static create(): Account { + const keyPair = nacl.sign.keyPair(); + const address = new AccountAddress({data: Account.authKey(keyPair.publicKey).toUint8Array()}); + return new Account(keyPair, address); + } + + /** + * Creates new account with provided private key + * + * @param privateKey Hex - private key of the account + * @returns Account + */ + static fromPrivateKey(privateKey: Hex): Account { + const keyPair = nacl.sign.keyPair.fromSeed(privateKey.toUint8Array().slice(0, 32)); + const address = new AccountAddress({data: Account.authKey(keyPair.publicKey).toUint8Array()}); + return new Account(keyPair, address); + } + + /** + * Creates new account with provided private key and address + * This is intended to be used for account that has it's key rotated + * + * @param privateKey Hex - private key of the account + * @param address AccountAddress - address of the account + * @returns Account + */ + static fromPrivateKeyAndAddress(privateKey: Hex, address: AccountAddress): Account { + const signingKey = nacl.sign.keyPair.fromSeed(privateKey.toUint8Array().slice(0, 32)); + return new Account(signingKey, address); + } + + /** + * Creates new account with bip44 path and mnemonics, + * @param path. (e.g. m/44'/637'/0'/0'/0') + * Detailed description: {@link https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki} + * @param mnemonics. + * @returns AptosAccount + */ + static fromDerivationPath(path:string, mnemonics: string): Account { + if (!Account.isValidPath(path)) { + throw new Error("Invalid derivation path"); + } + + const normalizeMnemonics = mnemonics + .trim() + .split(/\s+/) + .map((part) => part.toLowerCase()) + .join(" "); + + const { key } = derivePath(path, bytesToHex(bip39.mnemonicToSeedSync(normalizeMnemonics))); + + const signingKey = nacl.sign.keyPair.fromSeed(key.slice(0, 32)); + const address = new AccountAddress({data: Account.authKey(signingKey.publicKey).toUint8Array()}); + + return new Account(signingKey, address); + } + + /** + * Check's if the derive path is valid + */ + static isValidPath(path: string): boolean { + return /^m\/44'\/637'\/[0-9]+'\/[0-9]+'\/[0-9]+'+$/.test(path); + } + + /** + * This key enables account owners to rotate their private key(s) + * associated with the account without changing the address that hosts their account. + * See here for more info: {@link https://aptos.dev/concepts/accounts#single-signer-authentication} + * @returns Authentication key for the associated account + */ + @Memoize() + static authKey(publicKey: Uint8Array): Hex { + const pubKey = new Ed25519PublicKey(publicKey); + const authKey = AuthenticationKey.fromEd25519PublicKey(pubKey); + return authKey.data; + } + + sign(data: Uint8Array): Hex { + const signature = nacl.sign.detached(data, this._signingKey.secretKey); + return new Hex({ data: signature }); + } + + verifySignature(message: Hex, signature: Hex): boolean { + const rawMessage = message.toUint8Array(); + const rawSignature = signature.toUint8Array(); + return nacl.sign.detached.verify(rawMessage, rawSignature, this._signingKey.publicKey); + } +} \ No newline at end of file diff --git a/ecosystem/typescript/sdk_v2/src/core/index.ts b/ecosystem/typescript/sdk_v2/src/core/index.ts index 4328f607f82da..bdc59c5a8d8b2 100644 --- a/ecosystem/typescript/sdk_v2/src/core/index.ts +++ b/ecosystem/typescript/sdk_v2/src/core/index.ts @@ -2,5 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 export * from "./account_address"; +export * from "./account"; export * from "./common"; export * from "./hex"; diff --git a/ecosystem/typescript/sdk_v2/src/utils/hd-key.ts b/ecosystem/typescript/sdk_v2/src/utils/hd-key.ts new file mode 100644 index 0000000000000..79d7d06e47b1c --- /dev/null +++ b/ecosystem/typescript/sdk_v2/src/utils/hd-key.ts @@ -0,0 +1,76 @@ +import nacl from "tweetnacl"; +import { hmac } from "@noble/hashes/hmac"; +import { sha512 } from "@noble/hashes/sha512"; +import { hexToBytes } from "@noble/hashes/utils"; + +export type Keys = { + key: Uint8Array; + chainCode: Uint8Array; +}; + +const pathRegex = /^m(\/[0-9]+')+$/; + +const replaceDerive = (val: string): string => val.replace("'", ""); + +const HMAC_KEY = "ed25519 seed"; +const HARDENED_OFFSET = 0x80000000; + +export const getMasterKeyFromSeed = (seed: string): Keys => { + const h = hmac.create(sha512, HMAC_KEY); + const I = h.update(hexToBytes(seed)).digest(); + const IL = I.slice(0, 32); + const IR = I.slice(32); + return { + key: IL, + chainCode: IR, + }; +}; + +export const CKDPriv = ({ key, chainCode }: Keys, index: number): Keys => { + const buffer = new ArrayBuffer(4); + new DataView(buffer).setUint32(0, index); + const indexBytes = new Uint8Array(buffer); + const zero = new Uint8Array([0]); + const data = new Uint8Array([...zero, ...key, ...indexBytes]); + + const I = hmac.create(sha512, chainCode).update(data).digest(); + const IL = I.slice(0, 32); + const IR = I.slice(32); + return { + key: IL, + chainCode: IR, + }; +}; + +export const getPublicKey = (privateKey: Uint8Array, withZeroByte = true): Uint8Array => { + const keyPair = nacl.sign.keyPair.fromSeed(privateKey); + const signPk = keyPair.secretKey.subarray(32); + const zero = new Uint8Array([0]); + return withZeroByte ? new Uint8Array([...zero, ...signPk]) : signPk; +}; + +export const isValidPath = (path: string): boolean => { + if (!pathRegex.test(path)) { + return false; + } + return !path + .split("/") + .slice(1) + .map(replaceDerive) + .some(Number.isNaN as any); +}; + +export const derivePath = (path: string, seed: string, offset = HARDENED_OFFSET): Keys => { + if (!isValidPath(path)) { + throw new Error("Invalid derivation path"); + } + + const { key, chainCode } = getMasterKeyFromSeed(seed); + const segments = path + .split("/") + .slice(1) + .map(replaceDerive) + .map((el) => parseInt(el, 10)); + + return segments.reduce((parentKeys, segment) => CKDPriv(parentKeys, segment + offset), { key, chainCode }); +}; diff --git a/ecosystem/typescript/sdk_v2/src/utils/index.ts b/ecosystem/typescript/sdk_v2/src/utils/index.ts new file mode 100644 index 0000000000000..7db3c0e31345e --- /dev/null +++ b/ecosystem/typescript/sdk_v2/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./hd-key"; +export * from "./memoize-decorator"; \ No newline at end of file diff --git a/ecosystem/typescript/sdk_v2/src/utils/memoize-decorator.ts b/ecosystem/typescript/sdk_v2/src/utils/memoize-decorator.ts new file mode 100644 index 0000000000000..e35a6cc018c86 --- /dev/null +++ b/ecosystem/typescript/sdk_v2/src/utils/memoize-decorator.ts @@ -0,0 +1,151 @@ +/** + * Credits to https://github.com/darrylhodgins/typescript-memoize + */ + +/* eslint-disable no-param-reassign */ +/* eslint-disable no-restricted-syntax */ + +interface MemoizeArgs { + // ttl in milliseconds for cached items. After `ttlMs`, cached items are evicted automatically. If no `ttlMs` + // is provided, cached items won't get auto-evicted. + ttlMs?: number; + // produces the cache key based on `args`. + hashFunction?: boolean | ((...args: any[]) => any); + // cached items can be taged with `tags`. `tags` can be used to evict cached items + tags?: string[]; +} + +export function Memoize(args?: MemoizeArgs | MemoizeArgs["hashFunction"]) { + let hashFunction: MemoizeArgs["hashFunction"]; + let ttlMs: MemoizeArgs["ttlMs"]; + let tags: MemoizeArgs["tags"]; + + if (typeof args === "object") { + hashFunction = args.hashFunction; + ttlMs = args.ttlMs; + tags = args.tags; + } else { + hashFunction = args; + } + + return (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor) => { + if (descriptor.value != null) { + descriptor.value = getNewFunction(descriptor.value, hashFunction, ttlMs, tags); + } else if (descriptor.get != null) { + descriptor.get = getNewFunction(descriptor.get, hashFunction, ttlMs, tags); + } else { + throw new Error("Only put a Memoize() decorator on a method or get accessor."); + } + }; +} + +export function MemoizeExpiring(ttlMs: number, hashFunction?: MemoizeArgs["hashFunction"]) { + return Memoize({ + ttlMs, + hashFunction, + }); +} + +const clearCacheTagsMap: Map[]> = new Map(); + +export function clear(tags: string[]): number { + const cleared: Set> = new Set(); + for (const tag of tags) { + const maps = clearCacheTagsMap.get(tag); + if (maps) { + for (const mp of maps) { + if (!cleared.has(mp)) { + mp.clear(); + cleared.add(mp); + } + } + } + } + return cleared.size; +} + +function getNewFunction( + originalMethod: () => void, + hashFunction?: MemoizeArgs["hashFunction"], + ttlMs: number = 0, + tags?: MemoizeArgs["tags"], +) { + const propMapName = Symbol("__memoized_map__"); + + // The function returned here gets called instead of originalMethod. + // eslint-disable-next-line func-names + return function (...args: any[]) { + let returnedValue: any; + + // @ts-ignore + const that: any = this; + + // Get or create map + // eslint-disable-next-line no-prototype-builtins + if (!that.hasOwnProperty(propMapName)) { + Object.defineProperty(that, propMapName, { + configurable: false, + enumerable: false, + writable: false, + value: new Map(), + }); + } + const myMap: Map = that[propMapName]; + + if (Array.isArray(tags)) { + for (const tag of tags) { + if (clearCacheTagsMap.has(tag)) { + clearCacheTagsMap.get(tag)!.push(myMap); + } else { + clearCacheTagsMap.set(tag, [myMap]); + } + } + } + + if (hashFunction || args.length > 0 || ttlMs > 0) { + let hashKey: any; + + // If true is passed as first parameter, will automatically use every argument, passed to string + if (hashFunction === true) { + hashKey = args.map((a) => a.toString()).join("!"); + } else if (hashFunction) { + hashKey = hashFunction.apply(that, args); + } else { + // eslint-disable-next-line prefer-destructuring + hashKey = args[0]; + } + + const timestampKey = `${hashKey}__timestamp`; + let isExpired: boolean = false; + if (ttlMs > 0) { + if (!myMap.has(timestampKey)) { + // "Expired" since it was never called before + isExpired = true; + } else { + const timestamp = myMap.get(timestampKey); + isExpired = Date.now() - timestamp > ttlMs; + } + } + + if (myMap.has(hashKey) && !isExpired) { + returnedValue = myMap.get(hashKey); + } else { + returnedValue = originalMethod.apply(that, args as any); + myMap.set(hashKey, returnedValue); + if (ttlMs > 0) { + myMap.set(timestampKey, Date.now()); + } + } + } else { + const hashKey = that; + if (myMap.has(hashKey)) { + returnedValue = myMap.get(hashKey); + } else { + returnedValue = originalMethod.apply(that, args as any); + myMap.set(hashKey, returnedValue); + } + } + + return returnedValue; + }; +} \ No newline at end of file From dbf2e51409cc6dec718a81daccb9e9bbe49c2f58 Mon Sep 17 00:00:00 2001 From: Jin Date: Thu, 14 Sep 2023 21:03:42 -0700 Subject: [PATCH 7/7] add account unit test --- .../typescript/sdk_v2/src/core/account.ts | 48 ++++++------ .../sdk_v2/tests/unit/account.test.ts | 75 +++++++++++++++++++ 2 files changed, 101 insertions(+), 22 deletions(-) create mode 100644 ecosystem/typescript/sdk_v2/tests/unit/account.test.ts diff --git a/ecosystem/typescript/sdk_v2/src/core/account.ts b/ecosystem/typescript/sdk_v2/src/core/account.ts index 61de0a503f765..a9f4dc8636506 100644 --- a/ecosystem/typescript/sdk_v2/src/core/account.ts +++ b/ecosystem/typescript/sdk_v2/src/core/account.ts @@ -9,10 +9,11 @@ import { Hex } from "./hex"; import { bytesToHex } from "@noble/hashes/utils"; import { Ed25519PublicKey } from "../crypto/ed25519"; import { AuthenticationKey } from "../crypto/authentication_key"; +import { HexInput } from "../types"; /** * Class for creating and managing account on Aptos network - * + * * Use this class to create accounts, sign transactions, and more. */ export class Account { @@ -28,7 +29,7 @@ export class Account { /** * Public key of the account - * + * * @returns Hex - public key of the account */ get publicKey(): Hex { @@ -37,7 +38,7 @@ export class Account { /** * Private key of the account - * + * * @returns Hex - private key of the account */ get privateKey(): Hex { @@ -46,7 +47,7 @@ export class Account { /** * Address of the account - * + * * @returns AccountAddress - address of the account */ get accountAddress(): AccountAddress { @@ -55,7 +56,7 @@ export class Account { /** * private constructor for Account - * + * * This method is private because it should only be called by the factory static methods. * @returns Account */ @@ -66,24 +67,25 @@ export class Account { /** * Creates new account with random private key and address - * + * * @returns Account */ static create(): Account { const keyPair = nacl.sign.keyPair(); - const address = new AccountAddress({data: Account.authKey(keyPair.publicKey).toUint8Array()}); + const address = new AccountAddress({ data: Account.authKey(keyPair.publicKey).toUint8Array() }); return new Account(keyPair, address); } /** * Creates new account with provided private key - * + * * @param privateKey Hex - private key of the account * @returns Account */ - static fromPrivateKey(privateKey: Hex): Account { - const keyPair = nacl.sign.keyPair.fromSeed(privateKey.toUint8Array().slice(0, 32)); - const address = new AccountAddress({data: Account.authKey(keyPair.publicKey).toUint8Array()}); + static fromPrivateKey(privateKey: HexInput): Account { + const privatekeyHex = Hex.fromHexInput({ hexInput: privateKey }); + const keyPair = nacl.sign.keyPair.fromSeed(privatekeyHex.toUint8Array().slice(0, 32)); + const address = new AccountAddress({ data: Account.authKey(keyPair.publicKey).toUint8Array() }); return new Account(keyPair, address); } @@ -95,8 +97,9 @@ export class Account { * @param address AccountAddress - address of the account * @returns Account */ - static fromPrivateKeyAndAddress(privateKey: Hex, address: AccountAddress): Account { - const signingKey = nacl.sign.keyPair.fromSeed(privateKey.toUint8Array().slice(0, 32)); + static fromPrivateKeyAndAddress(privateKey: HexInput, address: AccountAddress): Account { + const privatekeyHex = Hex.fromHexInput({ hexInput: privateKey }); + const signingKey = nacl.sign.keyPair.fromSeed(privatekeyHex.toUint8Array().slice(0, 32)); return new Account(signingKey, address); } @@ -107,7 +110,7 @@ export class Account { * @param mnemonics. * @returns AptosAccount */ - static fromDerivationPath(path:string, mnemonics: string): Account { + static fromDerivationPath(path: string, mnemonics: string): Account { if (!Account.isValidPath(path)) { throw new Error("Invalid derivation path"); } @@ -121,7 +124,7 @@ export class Account { const { key } = derivePath(path, bytesToHex(bip39.mnemonicToSeedSync(normalizeMnemonics))); const signingKey = nacl.sign.keyPair.fromSeed(key.slice(0, 32)); - const address = new AccountAddress({data: Account.authKey(signingKey.publicKey).toUint8Array()}); + const address = new AccountAddress({ data: Account.authKey(signingKey.publicKey).toUint8Array() }); return new Account(signingKey, address); } @@ -140,20 +143,21 @@ export class Account { * @returns Authentication key for the associated account */ @Memoize() - static authKey(publicKey: Uint8Array): Hex { + static authKey(publicKey: HexInput): Hex { const pubKey = new Ed25519PublicKey(publicKey); const authKey = AuthenticationKey.fromEd25519PublicKey(pubKey); return authKey.data; } - sign(data: Uint8Array): Hex { - const signature = nacl.sign.detached(data, this._signingKey.secretKey); + sign(data: HexInput): Hex { + const hex = Hex.fromHexInput({ hexInput: data }); + const signature = nacl.sign.detached(hex.toUint8Array(), this._signingKey.secretKey); return new Hex({ data: signature }); } - verifySignature(message: Hex, signature: Hex): boolean { - const rawMessage = message.toUint8Array(); - const rawSignature = signature.toUint8Array(); + verifySignature(message: HexInput, signature: HexInput): boolean { + const rawMessage = Hex.fromHexInput({ hexInput: message }).toUint8Array(); + const rawSignature = Hex.fromHexInput({ hexInput: signature }).toUint8Array(); return nacl.sign.detached.verify(rawMessage, rawSignature, this._signingKey.publicKey); } -} \ No newline at end of file +} diff --git a/ecosystem/typescript/sdk_v2/tests/unit/account.test.ts b/ecosystem/typescript/sdk_v2/tests/unit/account.test.ts new file mode 100644 index 0000000000000..2e9b290c4c258 --- /dev/null +++ b/ecosystem/typescript/sdk_v2/tests/unit/account.test.ts @@ -0,0 +1,75 @@ +// Copyright © Aptos Foundation +// SPDX-License-Identifier: Apache-2.0 + +import { Account } from "../../src/core/account"; +import { AccountAddress } from "../../src/core/account_address"; +import { Hex } from "../../src/core/hex"; + +// eslint-disable-next-line max-len +const mockPrivateKey = Hex.fromString({ + str: "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5de19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c", +}); +const mockAddress = AccountAddress.fromString({ + input: "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa", +}); +const mockPublicKey = Hex.fromString({ str: "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c" }); +const mnemonic = "shoot island position soft burden budget tooth cruel issue economy destroy above"; + +describe("Account", () => { + it("should create an instance of Account correctly without error", () => { + const account = Account.create(); + expect(account).toBeInstanceOf(Account); + }); + + it("should create a new account from a provided private key", () => { + const newAccount = Account.fromPrivateKey(mockPrivateKey.toString()); + expect(newAccount).toBeInstanceOf(Account); + expect(newAccount.privateKey.toString()).toEqual(mockPrivateKey.toString()); + expect(newAccount.publicKey.toString()).toEqual(mockPublicKey.toString()); + expect(newAccount.accountAddress.toString()).toEqual(mockAddress.toString()); + }); + + it("should create a new account from a provided private key and address", () => { + const newAccount = Account.fromPrivateKeyAndAddress(mockPrivateKey.toString(), mockAddress); + expect(newAccount).toBeInstanceOf(Account); + expect(newAccount.privateKey.toString()).toEqual(mockPrivateKey.toString()); + expect(newAccount.publicKey.toString()).toEqual(mockPublicKey.toString()); + expect(newAccount.accountAddress.toString()).toEqual(mockAddress.toString()); + }); + + it("should create a new account from a provided private key and address", () => { + const newAccount = Account.fromPrivateKeyAndAddress(mockPrivateKey.toString(), mockAddress); + expect(newAccount).toBeInstanceOf(Account); + expect(newAccount.privateKey.toString()).toEqual(mockPrivateKey.toString()); + }); + + it("should create a new account from a bip44 path and mnemonics", () => { + const address = "0x07968dab936c1bad187c60ce4082f307d030d780e91e694ae03aef16aba73f30"; + const bip44Path = "m/44'/637'/0'/0'/0'"; + const newAccount = Account.fromDerivationPath(bip44Path, mnemonic); + expect(newAccount.accountAddress.toString()).toEqual(address); + }); + + it("should check if a derivation path is valid", () => { + const validPath = "m/44'/637'/0'/0'/0'"; // Valid path + const invalidPath = "invalid/path"; // Invalid path + expect(Account.isValidPath(validPath)).toBe(true); + expect(Account.isValidPath(invalidPath)).toBe(false); + }); + + it("should return the authentication key for a public key", () => { + const authKey = Account.authKey(mockPublicKey.toUint8Array()); + expect(authKey).toBeInstanceOf(Hex); + expect(authKey.toString()).toEqual(mockAddress.toString()); + }); + + it("should sign data, return a Hex signature, and verify", () => { + const account = Account.fromPrivateKeyAndAddress(mockPrivateKey.toString(), mockAddress); + const messageHex = "0x7777"; + const expectedSignedMessage = + "0xc5de9e40ac00b371cd83b1c197fa5b665b7449b33cd3cdd305bb78222e06a671a49625ab9aea8a039d4bb70e275768084d62b094bc1b31964f2357b7c1af7e0d"; + expect(account.sign(messageHex).toString()).toEqual(expectedSignedMessage); + expect(account.verifySignature(messageHex, expectedSignedMessage)).toBe(true); + expect(account.verifySignature(messageHex, expectedSignedMessage)).toBe(true); + }); +});