From 99e51ba78ad07b68943025fa4a2cba4874e3e290 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Fri, 10 Jan 2025 15:26:00 -0800 Subject: [PATCH] Copy README from libnemo which will need to be rewritten. Add nano-webgl-js to devdeps for comparison testing. Extract random hash generation to test config file. --- README.md | 282 ++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 8 ++ package.json | 1 + test/CONFIG.mjs | 8 ++ test/index.html | 1 + test/test.mjs | 20 ++-- 6 files changed, 307 insertions(+), 13 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a10272 --- /dev/null +++ b/README.md @@ -0,0 +1,282 @@ + + +# libnemo +libnemo is a fork of the nanocurrency-web toolkit. It is used for client-side +implementations of Nano cryptocurrency wallets and enables building web-based +applications that can work even while offline. libnemo supports managing +wallets, deriving accounts, signing blocks, and more. + +It utilizes the Web Crypto API which is native to all modern browsers; as such, +it has only a required dependency in order to work with the BLAKE2b algorithm. +Optionally, Ledger device dependencies can be installed to enable Ledger +hardware wallet support. + +## Features +* Generate new BIP-32 hierarchial deterministic (HD) wallets with a BIP-39 +mnemonic phrase and the Nano path registered with BIP-44. Used by Ledger +hardware wallet. +* Generate new BLAKE2b wallets with a BIP-39 mnemonic phrases. Original method +described by nano spec. +* Import wallets with a mnemonic phrase or a seed. +* Derive indexed accounts with a Nano address and a public-private keypair. +* Create, sign, and verify send, receive, and change blocks. +* Get account info and process blocks on the network while online. +* Manage known addresses with a rolodex. +* Sign and verify arbitrary strings with relevant keys. +* Validate entropy, seeds, mnemonic phrases, and Nano addresses. +* Convert Nano unit denominations. +* Run in modern web browsers and mobile frameworks built with Javascript without +server-side NodeJS functions. + +## Installation +### From NPM + +```console +npm install libnemo +``` + +## Usage +#### ⚠️ The examples below should never be used for real transactions! ⚠️ + +### Wallets and accounts +At its core, a wallet is a hexadecimal string called a seed. From this seed, +millions of unique accounts can be deterministically derived. The first account +in a wallet starts at index 0. + +For clarity, the following terms are used throughout the library: + * BIP-32 - Defines how hierarchical determinstic (HD) wallets are generated + * BIP-39 - Defines how mnemonic phrases are generated + * BIP-44 - Expands on BIP-32 to define how an enhanced derivation path can + allow a single wallet to store multiple currencies + +libnemo is able to generate and import HD and BLAKE2b wallets, and it can derive +accounts for both. An HD wallet seed is 128 characters while a BLAKE2b wallet +seed is 64 characters. For enhanced security, libnemo requires a password to +create or import wallets, and wallets are initialized in a locked state. More +advanced implementations can provide their own CryptoKey instead of a password. +Refer to the documentation on each class factory method for specific usage. + +```javascript +import { Bip44Wallet, Blake2bWallet } from 'libnemo' + +const wallet = await Bip44Wallet.create(password) +const wallet = await Bip44Wallet.fromEntropy(password, entropy, salt?) +const wallet = await Bip44Wallet.fromMnemonic(password, mnemonic, salt?) +const wallet = await Bip44Wallet.fromSeed(password, seed) + +const wallet = await Bip44Wallet.create(password) +const wallet = await Bip44Wallet.fromSeed(password, seed) +const wallet = await Bip44Wallet.fromMnemonic(password, mnemonic) +``` + +```javascript +try { + const unlockResult = await wallet.unlock(password) +} catch(err) { + console.log(err) +} +console.log(unlockResult) // true if successfully unlocked + +const { mnemonic, seed } = wallet + +const accounts = await wallet.accounts(from?, to?) +const firstAccount = accounts[0] +const { address, publicKey, privateKey, index } = firstAccount + +const nodeUrl = 'https://nano-node.example.com/' +await firstAccount.refresh(nodeUrl) // online +const { frontier, balance, representative } = firstAccount +``` + +### Blocks +Blocks do not contain transaction amounts. Instead, they contain stateful +balance changes only. For example, if sending Ӿ5 from an account with a balance +of Ӿ20, the send block would contain `balance: Ӿ15` (psuedocode for +demonstration purposes and not a literal depiction). This can be difficult to +track, so libnemo provides the convenience of specifying an amount to send or +receive and calculates the balance change itself. + +All blocks are 'state' types, but they are interpreted as one of three different +subtypes based on the data they contain: send, receive, or change +representative. libnemo implements them as the following classes: + +* SendBlock: the Nano balance of the account decreases +* ReceivBlock: the Nano balance of the account increases and requires a matching +SendBlock +* ChangeBlock: the representative for the account changes while the Nano balance +does not + +_Nano protocol allows changing the representative at the same time as a balance +change. libnemo does not implement this for purposes of clarity; all +ChangeBlock objects will maintain the same Nano balance._ + +Always fetch the most up to date information for the account from the network +using the +[account_info RPC command](https://docs.nano.org/commands/rpc-protocol/#account_info) +which can then be used to populate the block parameters. + +Blocks require a small proof-of-work that must be calculated for the block to be +accepted by the network. This can be provided when creating the block, or a +public node that allows the +[work_generate RPC command](https://docs.nano.org/commands/rpc-protocol/#work_generate) +can be used. + +Finally, the block must be signed with the private key of the account. libnemo +enables this to be done offline if desired. After being signed, the block can +be published to the network with the +[process RPC command](https://docs.nano.org/commands/rpc-protocol/#process). + +#### Creating blocks +```javascript +import { SendBlock, ReceiveBlock, ChangeBlock } from 'libnemo' + +const send = new SendBlock( + 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // sender + '5618869000000000000000000000000', // current balance + 'nano_3phqgrqbso99xojkb1bijmfryo7dy1k38ep1o3k3yrhb7rqu1h1k47yu78gz', // recipient + '2000000000000000000000000000000', // amount to send + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', // representative + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block + 'fbffed7c73b61367' // PoW nonce (optional at first but required to process) +) + +const receive = new ReceiveBlock( + 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // recipient + '18618869000000000000000000000000', // current balance + 'CBC911F57B6827649423C92C88C0C56637A4274FF019E77E24D61D12B5338783', // origin (hash of matching send block) + '7000000000000000000000000000000', // amount that was sent + 'nano_1stofnrxuz3cai7ze75o174bpm7scwj9jn3nxsn8ntzg784jf1gzn1jjdkou', // representative + '92BA74A7D6DC7557F3EDA95ADC6341D51AC777A0A6FF0688A5C492AB2B2CB40D', // hash of previous block + 'c5cf86de24b24419' // PoW nonce (optional at first but required to process) +) + +const change = new ChangeBlock( + 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d', // account redelegating vote weight + '3000000000000000000000000000000', // current balance + 'nano_1anrzcuwe64rwxzcco8dkhpyxpi8kd7zsjc1oeimpc3ppca4mrjtwnqposrs', // new representative + '128106287002E595F479ACD615C818117FCB3860EC112670557A2467386249D4', // hash of previous block + '0000000000000000' // PoW nonce (optional at first but required to process) +) +``` + +#### Signing a block +```javascript +const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143' +try { + await block.sign(privateKey) +} catch (err) { + console.log(err) +} +``` + +#### Caculating proof-of-work from an online service +```javascript +const node = new Rpc('https://nano-node.example.com/') +try { + await block.pow('https://nano-node.example.com/') +} catch (err) { + console.log(err) +} +``` + +#### Processing a block on the network +```javascript +const node = new Rpc('https://nano-node.example.com', 'nodes-api-key') +try { + const hash = await block.process('https://nano-node.example.com/') +} catch (err) { + console.log(err) +} +``` + +### Tools +#### Converting Nano denominations +Raw values are the native unit of exchange throughout libnemo and are +represented by the primitive bigint data type. Other supported denominations +are as follows: +| Unit | Raw | +|-------|-----| +| RAI | 1024 raw | +| NYANO | 1024 raw | +| KRAI | 1027 raw | +| PICO | 1027 raw | +| MRAI | 1030 raw | +| NANO | 1030 raw | +| KNANO | 1033 raw | +| MNANO | 1036 raw | + +```javascript +import { Tools } from 'libnemo' +// Denominations are case-insensitive +const oneNanoToRaw = Tools.convert('1', 'NANO', 'RAW') // 1000000000000000000000000000000 +const oneNonillionRawToNano = Tools.convert('1000000000000000000000000000000', 'RAW', 'NANO') // 1 +const oneThousandNyanoToPico = Tools.convert('1000', 'nYaNo', 'pico') //1 +const oneThousandPicoToNano = Tools.convert('1000', 'pico', 'NANO') // 1 +``` + +#### Verifying signatures and signing anything with the private key +Since cryptocurrencies like Nano uses asymmetric keys to sign and verify blocks +and transactions, a Nano account itself can be used to sign arbitrary data +with its private key and verify signatures from other accounts with their public +keys. + +For example, a client-side login can be implemented by challenging an account +owner to sign their email address using their private key: + +```javascript +import { Tools } from 'libnemo' + +const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143' +const publicKey = '5B65B0E8173EE0802C2C3E6C9080D1A16B06DE1176C938A924F58670904E82C4' +const signature = await Tools.sign(privateKey, 'johndoe@example.com') +const isValid = await Tools.verify(publicKey, signature, 'johndoe@example.com') +``` + +Ownership of a Nano address can also be proven by challenging the account owner +to sign an arbitrary string and then validating the signature with the Nano +account address. +```javascript +import { Tools } from 'libnemo' + +const address = 'nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d' +const privateKey = '3BE4FC2EF3F3B7374E6FC4FB6E7BB153F8A2998B3B3DAB50853EABE128024143' +const randomData = new Entropy().hex + +const signature = await Tools.sign(privateKey, randomData) +const publicKey = new Account(address).publicKey +const isValid = await Tools.verify(publicKey, signature, randomData) +``` + +#### Validate a Nano account address +```javascript +import { Tools } from 'libnemo' + +const valid = Account.validate('nano_1pu7p5n3ghq1i1p4rhmek41f5add1uh34xpb94nkbxe8g4a6x1p69emk8y1d') +``` + +## Tests +Test vectors were retrieved from the following publicly-available locations: + * Nano (BIP-44): https://docs.nano.org/integration-guides/key-management/#test-vectors + * Trezor (BIP-39): https://github.com/trezor/python-mnemonic/blob/master/vectors.json + * BIP-32: https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki#user-content-Test_Vectors +Another set of test vectors were created for libnemo based on the Trezor set. +These extra test vectors were generated purely to test uncommon yet valid +mnemonic phrase lengths like 15 or 18 words. +#### ⚠️ The test vectors should never be used for real transactions! ⚠️ + +## Building +* `npm run build`: compile and build +* `npm run test`: all of the above, run tests, and print results to the console +* `npm run test:coverage`: all of the above, calculate code coverage, and print +code coverage to the console +* `npm run test:coverage:report`: all of the above, and open an HTML code +coverage report in the browser (requires lcov and xdg-open) + +## Donations +If you find this library helpful, please consider tipping the developer. +``` +nano_1zosoqs47yt47bnfg7sdf46kj7asn58b7uzm9ek95jw7ccatq37898u1zoso +``` diff --git a/package-lock.json b/package-lock.json index 1f7ebe8..9d89fcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@webgpu/types": "^0.1.52", "esbuild": "^0.24.2", "esbuild-plugin-glsl": "^1.2.2", + "nano-webgl-pow": "^1.1.1", "typescript": "^5.7.3" }, "funding": { @@ -516,6 +517,13 @@ "esbuild": "0.x.x" } }, + "node_modules/nano-webgl-pow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nano-webgl-pow/-/nano-webgl-pow-1.1.1.tgz", + "integrity": "sha512-IKAg7qx2y4n9dnT7tYYypOun/aV+35SfRxJCVnc63GboWQ5/woVIVAZcdX5VfXM1mLYBzADvXxoWZ39G3iPOFA==", + "dev": true, + "license": "MIT" + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", diff --git a/package.json b/package.json index 8565097..0006b2b 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "@webgpu/types": "^0.1.52", "esbuild": "^0.24.2", "esbuild-plugin-glsl": "^1.2.2", + "nano-webgl-pow": "^1.1.1", "typescript": "^5.7.3" }, "type": "module", diff --git a/test/CONFIG.mjs b/test/CONFIG.mjs index 4ade381..7afb956 100644 --- a/test/CONFIG.mjs +++ b/test/CONFIG.mjs @@ -1,6 +1,14 @@ // SPDX-FileCopyrightText: 2025 Chris Duncan // SPDX-License-Identifier: GPL-3.0-or-later +export function random (size = 32) { + const bytes = new Uint8Array(size) + crypto.getRandomValues(bytes) + let hex = '' + for (let i = 0; i < size; i++) hex += bytes[i].toString(16).padStart(2, '0') + return hex +} + export function average (times) { let count = times.length, sum = 0, reciprocals = 0, logarithms = 0, truncated = 0, min = 0xffff, max = 0 times.sort() diff --git a/test/index.html b/test/index.html index 6e5156b..3cb8c5d 100644 --- a/test/index.html +++ b/test/index.html @@ -8,6 +8,7 @@ SPDX-License-Identifier: GPL-3.0-or-later + diff --git a/test/test.mjs b/test/test.mjs index 7f805c6..41b1d93 100644 --- a/test/test.mjs +++ b/test/test.mjs @@ -3,18 +3,16 @@ 'use strict' -import { assert, average, print, skip, suite, test } from './CONFIG.mjs' +import { assert, average, print, random, skip, suite, test } from './CONFIG.mjs' import { NanoPowGl, NanoPowGpu } from '../dist/main.min.js' await suite('Block performance', async () => { - const COUNT = 0x20 + const COUNT = 0xf await test(`NanoPowGpu: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => { const times = [] - const random = new Uint8Array(32) for (let i = 0; i < COUNT; i++) { - crypto.getRandomValues(random) - const hash = random.reduce((curr, next) => { return `${curr}${next.toString(16).padStart(2, '0')}` }, '') + const hash = random() const start = performance.now() const work = await NanoPowGpu.search(hash) const end = performance.now() @@ -24,12 +22,10 @@ await suite('Block performance', async () => { print(times) }) - await skip(`NanoPowGl: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => { + await test(`NanoPowGl: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => { const times = [] - const random = new Uint8Array(32) for (let i = 0; i < COUNT; i++) { - crypto.getRandomValues(random) - const hash = random.reduce((curr, next) => { return `${curr}${next}` }, '') + const hash = random() const start = performance.now() const work = await NanoPowGl.search(hash) const end = performance.now() @@ -39,16 +35,14 @@ await suite('Block performance', async () => { print(times) }) - await skip(`nano-webgl-pow: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => { + await test(`nano-webgl-pow: Calculate proof-of-work for ${COUNT} unique send block hashes`, async () => { //@ts-expect-error window.NanoWebglPow.width = 256 * Math.max(1, Math.floor(navigator.hardwareConcurrency)) //@ts-expect-error window.NanoWebglPow.height = 256 * Math.max(1, Math.floor(navigator.hardwareConcurrency)) const times = [] - const random = new Uint8Array(32) for (let i = 0; i < COUNT; i++) { - crypto.getRandomValues(random) - const hash = random.reduce((curr, next) => { return `${curr}${next}` }, '') + const hash = random() const start = performance.now() const work = await new Promise(resolve => { //@ts-expect-error -- 2.34.1