Skip to content

Generate, validate, create, and convert WIFs and PayAddress.

License

Notifications You must be signed in to change notification settings

dashhive/DashKeys.js

Repository files navigation

Generate, validate, and convert DASH WIFs and Pay Addresses.
(Base58Check encoding/decoding for Private Keys and Public Key Hashes)

WIF:     XCGKuZcKDjNhx8DaNKK4xwMMNzspaoToT6CafJAbBfQTi57buhLK
Address: XrZJJfEKRNobcuwWKTD3bDu8ou7XSWPbc9

A fully-functional, production-ready reference implementation of Dash Keys - suitable for learning DASH specs and protocols, and porting to other languages.

Table of Contents

  • 🚀 Install
    • Terminal (CLI)
    • Node & Bundlers
    • Browser
  • 💪 Usage
  • ⚙️ API
  • 🧰 Developer Resources
    • 👩‍🏫 Glossary of Terms
      • Address, Check, Compressed, Private Key,
      • PubKey Hash, Public Key, Version, WIF, etc ...
    • 🦓 Fixtures (for testing)
      • The Canonical Dash "Zoomonic"
      • Anatomy of Addrs & WIFs
      • Troubleshooting Uncompressed Keys
      • Implementation Details
  • 📄 License

Install

Works in Command Line, Node, Bun, Bundlers, and Browsers

Terminal

npm install --location=global dashkeys-cli
dashkeys help

See DashKey's CLI README at github.com/dashhive/dashkeys-cli.js.

Node & Bundlers

Install

npm install --save dashkeys@1.x
npm install --save @dashincubator/secp256k1@1.x
let DashKeys = require("dashkeys");
let toHex = DashKeys.utils.bytesToHex;
let toBytes = DashKeys.utils.hexToBytes;

Browser

Install

<script src="https://unpkg.com/@dashincubator/secp256k1@1.x/secp256k1.js"></script>
<script src="https://unpkg.com/dashkeys@1.x/dashkeys.js"></script>
async function main() {
  "use strict";

  let DashKeys = window.DashKeys;
  let toHex = DashKeys.utils.bytesToHex;
  let toBytes = DashKeys.utils.hexToBytes;

  // ...
}

main().catch(function (err) {
  console.error("Fail:");
  console.error(err.stack || err);
});

Usage

let wif = await DashKeys.utils.generateWifNonHd();
// ex: "XCGKuZcKDjNhx8DaNKK4xwMMNzspaoToT6CafJAbBfQTi57buhLK"

let addr = await DashKeys.wifToAddr(wif);
// ex: "XrZJJfEKRNobcuwWKTD3bDu8ou7XSWPbc9"

You can use DashKeys.privKeyToWif(privateKey) to encode Private Keys to WIFs:

let privBuf = toBytes(
  "1d2a6b22fcb5a29a5357eaf27b1444c623e5e580b66ac5f1109e2778a0ffb950",
);
let wif = await DashKeys.privKeyToWif(privateKey);
let addr = await DashKeys.wifToAddr(wif);

API

Dash Keys doesn't do anything that other Base58Check libraries don't do.

The purpose of this rewrite has been to provide, a simpler, lightweight, more streamlined production-ready reference implementation that takes advantage of modern, cross-platform JS-native APIs, such as WebCrypto.

However, it does (we think) do a better job at exposing a functions for how the Base58Check codec is used in practice (rather than everything it can theoretically be used for).

Common Conversions

// Bi-Directional Conversions
await DashKeys.addrToPkh(address); // PubKey Hash Uint8Array (ShaRipeBytes)
await DashKeys.pkhToAddr(hashBytes, { version }); // Address (Base58Check-encoded)
await DashKeys.pubkeyToAddr(pubBytes); // Address (Base58Check-encoded)
await DashKeys.privKeyToWif(privBytes, { version }); // WIF (Base58Check-encoded)
await DashKeys.wifToPrivKey(wif); // Private Key Uint8Array

// One-Way Conversions
await DashKeys.wifToAddr(wif); // Address (Base58Check-encoded)
await DashKeys.pubkeyToPkh(pubBytes); // shaRipeBytes Uint8Array

Note: these all output either Base58Check Strings, or Byte Arrays (Uint8Array).

Common Encode / Decode Options

{
  // (default) throw if check(sum) fails
  validate: true,

  // which encoding to use
  // (not all are valid all the time - it depends on the use)
  version: "mainnet|testnet|private|pkh|xprv|xpub|cc|4c|ef|8c",
}

Debugging Encoder / Decoder

// Base58Check Codec for all of Private Key and PubKey Hash (and X Keys)
await DashKeys.decode(b58cString, { validate }); // { version, type, check, etc }
await DashKeys.encodeKey(keyBytes, { version }); // Base58Check-encoded key

Decode Output:

{
  check: "<hex>",
  compressed: true,
  type: "private|pkh|xprv|xpub",
  version: "cc|4c|ef|8c",
  valid: true, // check matches

  // possible key types
  privateKey: "<hex>",
  pubKeyHash: "<hex>",
  xprv: "<hex>",
  xpub: "<hex>",
}

note: compressed only applies to Private Keys and is always true.
Remains in for compatibility, but not used.

Helpful Helper Utils

// Byte Utils (NOT async)
let toHex = DashKeys.utils.bytesToHex;
toHex(uint8Array); // hex String

let toBytes = DashKeys.utils.hexToBytes;
toBytes(hexString); // bytes Uint8Array

// Hash Utils
await DashKeys.utils.ripemd160sum(bytes); // hash bytes Uint8Array
await DashKeys.utils.sha256sum(bytes); // hash bytes Uint8Array

Swappable Secp256k1 Utils

We felt it was important to not strictly depend on our chosen secp256k1 implementation.
(that's why you have to manually install it as a dependency yourself)

Use these functions as-is, or overwrite them with your own implementation.

// Key Utils (over
await DashKeys.utils.generateWifNonHd({ version }); // WIF string (non-hd, dev tool)
await DashKeys.utils.toPublicKey(privBytes); // Public Key Bytes

Example Overwrite

You DO NOT need to do this, but you may if you wish:

let Secp256k1 = require("@noble/secp256k1");

DashKeys.utils.generateWifNonHd = async function (opts) {
  let privBytes = Secp256k1.utils.randomPrivateKey();
  let privateKey = toHex(privBytes);
  let version = opts.version ?? "cc";

  let wif = await DashKeys.encode(privBytes, { version });
  return wif;
};

DashKeys.utils.toPublicKey = async function (privBytes) {
  let isCompressed = true;
  let pubBytes = Secp256k1.getPublicKey(privBytes, isCompressed);

  return pubBytes;
};

Glossary

Here are bunches of terms by their canonical name, as well as a terse description.

Address

Also: Payment Address, Pay Addr, Addr

A Base58Check-encoded PubKey Hash.
(can NOT be reversed into the Public Key)
The encoding is like this:

  • Coin Version Public byte(s), which is 4c for DASH
  • Public Key Hash (20 bytes)
  • Check(sum) is 4 bytes of SHA-256(concat(coin, comp, pkh))
  • Base58Check is Base85(concat(coin, comp, pkh, check))
Version:     cc
PubKey Hash: ae14c8728915b492d9d77813bd8fddd91ce70948
Check:       ce08541e

Decoded:     ccae14c8728915b492d9d77813bd8fddd91ce70948ce08541e

Encoded:     XrZJJfEKRNobcuwWKTD3bDu8ou7XSWPbc9

Base X

A bespoke algorithm for arbitrary-width bit encoding.
Similar to (but not at all compatible with) Base64.
Bit-width is based on the given alphabet's number of characters.

Base58

A specific 58-character Base X alphabet.
The same is used for DASH as most cryptocurrencies.

123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz

Chosen to eliminate confusion between similar characters.
(0 and O, 1 and l and I, etc)

Base58Check

A Base58 encoding schema with prefixes and suffixes.
verisonBytes is added as a prefix (before encoding).
Some metadata may come after the data.
checkBytes are added as a suffix (before encoding). \

Base58(`${versionBytes}${dataBytes}${metaBytes}${checkBytes}`);

See also Address, Check and WIF

Check

Also: Base58 Checksum, Base58 Hash, Base58 SHA-256

The last 4 bytes of a decoded WIF or Addr.
These are the first 4 bytes of the SHA-256 Hash of the same.

See Address and WIF.

Compressed Byte

Also: Compression Flag, Recovery Bit, Y is Even / Odd Byte, Quadrant

A Base58Check private key has the suffix 0x01, the compression flag.
This indicates that Pub Key Hashes must not include the Y value.

See also: Public Key.

HD Key

Also: HD Wallet WIF

An HD Key is a Private Key or WIF generated from an HD Wallet.
These are recoverable from a Passphrase "Mnemonic".
(HD means Hierarchical-Deterministic, as in "generated from seed")

HD keys are almost always preferrable over non-HD keys.

See Dash HD, Dash Wallet.

Private Key

Also: PrivKey

Any 32 random bytes (256-bits) that can produce a valid Public Key.
The public key is produced by creating a point on a known curve.

In essence:

let privBuf = genRandom(32);
let pirvHex = toHex(privBuf);
let privNum = BigInt(`0x${privHex}`);

// magic secp256k1 curve values
let curveN =
  0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
let Gx =
  55066263022277343669578718895168534326250603453777594175500187360389116729240n;
let Gy =
  32670510020758816978083085130507043184471273380659243275938904335757337482424n;
let windowSize = 8;

let isWithinCurveOrder = 0n < privNum && privNum < curveN;
if (!isWithinCurveOrder) {
  throw new Error("not in curve order");
}

let BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, 1n);
let jPoint = BASE.multiply(privNum, { Gx, Gy, windowSize }); // fancy math
let pubPoint = jPoint.toAffine(); // fancy math

let isOdd = pubPoint.y % 2n;
let prefix = "02";
if (isOdd) {
  prefix = "03";
}

let x = pubPoint.x.toString(16).padStart(32, "0");
let pubHex = `${prefix}${x}`;

See https://github.com/dashhive/secp256k1.js.

PubKey Hash

Also: Public Key Hash, PKH, PubKeyHash

The public key is hashed with SHA-256.
That result is hashed with RIPEMD-160.

RIPEMD160(SHA256(PublicKey))

Public Key

Also: PubKey

An 32-byte X value, prefixed with a byte describing the Y value.
The indicator is 0x02, if Y is ever, or 0x03 if Y is odd. \

In essence:

let expectOdd = 0x03 === pubkey[0];
let xBuf = pubkey.subarray(1);
let xHex = toHex(xBuf);
let x = BigInt(xHex);

// magic secp256k1 curve values
let a = 0n;
let b = 7n;
let P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn;

function mod(a, b = P) {
  let result = a % b;
  if (result >= 0n) {
    return result;
  }
  return b + result;
}
let x2 = mod(x * x);
let x3 = mod(x2 * x);
let y2 = mod(x3 + a * x + b);

let y = curveSqrtMod(y2); // very complicated
let isYOdd = (y & 1n) === 1n;
if (expectOdd && !isYOdd) {
  y = mod(-y);
}

let pubPoint = { x: x, y: y };

See https://github.com/dashhive/secp256k1.js.

RIPEMD160

An old, deprecated hash 20-byte algorithm - similar to MD5.
We're stuck with it for the foreseeable future. Oh, well.

Version

Also: Base58Check Version, Coin Version, Privacy Byte

0xcc is used for DASH mainnet WIFs (Private Key).
0x4c is the prefix for Payment Addresses (PubKey Hash) .
These bytes Base58Encode to X, (for mystery, i.e. "DarkCoin").

0xef (Priv) and 0x8c (PKH) are used for DASH testnet.
These Base58Encode to Y.

For use with HD tools, this Base58Check codec also supports:
0x0488ade4, which Base58-encodes to the xprv prefix.
0x0488b21e, which Base58-encodes to the xpub prefix.
0x04358394 and 0x043587cf, which encode to tprv and tpub.

See Dash HD for more info about Extended Private Keys (xprv, xpriv) and Extended Public Keys (xpub).

WIF

Also: Wallet Import Format, Paper Wallet, Swipe Key, Private Key QR

A Base58Check-encoded Private Key.
(CAN be reversed into the Private Key)
The encoding is like this:

  • Coin Version Private byte(s), which is cc for DASH
  • Compression byte (always 0x01)
  • Private Key (32 bytes)
  • Checksum is 4 bytes of SHA-256(concat(coin, privkey, compression))
  • Base58Check is Base85(concat(coin, privkey, compression, checksum))
Version:     cc
Comp Byte:   01 (always)
Private Key: 1d2a6b22fcb5a29a5357eaf27b1444c623e5e580b66ac5f1109e2778a0ffb950
Checksum:    ec533f80

Decoded:     cc011d2a6b22fcb5a29a5357eaf27b1444c623e5e580b66ac5f1109e2778a0ffb950ec533f80

Encoded:     XCGKuZcKDjNhx8DaNKK4xwMMNzspaoToT6CafJAbBfQTi57buhLK

Zoomonic

The HD Wallet used for all testing fixtures in this ecosystem of code.
(chosen from the original Trezor / BIP-39 test fixtures)

See The Canonical Dash "Zoomonic".

Fixtures

For troubleshooting, debugging, etc.

The Canonical Dash "Zoomonic":

All keys used in this example - and across this ecosystem of DASH tools - are HD keys derived from the "Zoomonic":

Passphrase (Mnemonic)  :  zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong
Secret (Salt Password) :  TREZOR
HD Path                :  m/44'/5'/0'/0/0
WIF                    :  XCGKuZcKDjNhx8DaNKK4xwMMNzspaoToT6CafJAbBfQTi57buhLK
Addr                   :  XrZJJfEKRNobcuwWKTD3bDu8ou7XSWPbc9

Anatomy of Addrs & WIFs

dashkeys inspect --unsafe ./examples/m44_5_0_0-0.wif
    Version:     cc
    Private Key: 1d2a6b22fcb5a29a5357eaf27b1444c623e5e580b66ac5f1109e2778a0ffb950
    Compressed:  01
    Pay Addr:    XrZJJfEKRNobcuwWKTD3bDu8ou7XSWPbc9
    Check:       ec533f80
    Valid:       true

Correct Private Key

PrivateKey:   cc011d2a6b22fcb5a29a5357eaf27b1444c623e5e580b66ac5f1109e2778a0ffb950
  --------
  Version:    cc
  Comp Flag:  01 (Compressed)
  Priv Key:   1d2a6b22fcb5a29a5357eaf27b1444c623e5e580b66ac5f1109e2778a0ffb950
  --------
WIF:          XCGKuZcKDjNhx8DaNKK4xwMMNzspaoToT6CafJAbBfQTi57buhLK

Correct Pub Key Hash

PubKey:       0245ddd5edaa25313bb88ee0abd359f6f58ac38ed597d33a981f1799903633d902
  --------
  Comp Flag:  02 (Quadrant 2)
  X:          45ddd5edaa25313bb88ee0abd359f6f58ac38ed597d33a981f1799903633d902
  SHA256:     8e5abfc42a6d7529b860ce2b4b8889380db893438dc96430f597ddb455e85fdd
  *RMD160:    54408a877b83cb9706373918a430728f72f3d001 (*not used)
  PubKeyHash: ae14c8728915b492d9d77813bd8fddd91ce70948
  Check:      ce08541e
  Version:    4c
  --------
Pay Address:    XrZJJfEKRNobcuwWKTD3bDu8ou7XSWPbc9

Troubleshooting Uncompressed Keys

If you see these values, then you've mistakenly used uncompressed keys.

Incorrect Private Key (Uncompressed)

PrivateKey:   cc1d2a6b22fcb5a29a5357eaf27b1444c623e5e580b66ac5f1109e2778a0ffb950
  --------
  Version:    cc
  Comp Flag:  missing, or 00 (Uncompressed)
  Priv Key:   1d2a6b22fcb5a29a5357eaf27b1444c623e5e580b66ac5f1109e2778a0ffb950
  --------
WIF:             XCGKuZcKDjNhx8DaNKK4xwMMNzspaoToT6CafJAbBfQTi4vf57Ka (00 comp flag)
                 7qmhzDpsoPHhYXBZ2f8igQeEgRSZXmWdoh9Wq6hgvAcDrD3Arhr (no comp flag)

Incorrect Pub Key Hash (Uncompressed)

PubKey (X+Y): 04
              45ddd5edaa25313bb88ee0abd359f6f58ac38ed597d33a981f1799903633d902
              607c88b97231d7f1419c772a6c55d2ad6a7c478a66fdd28ac88c622383073d12
  --------
  Comp Flag:  04, or 'false' (uncompressed)
  X:          45ddd5edaa25313bb88ee0abd359f6f58ac38ed597d33a981f1799903633d902
  Y:          607c88b97231d7f1419c772a6c55d2ad6a7c478a66fdd28ac88c622383073d12
  SHA256:     85c03bf3ba5042d2e7a84f0fcc969a7753a91e9c5c299062e1fdf7e0506b5f66
  *RMD160:    b9d17c4c4fb6307ba78c8d4853ed07bd7e4c9f5a (*not used)
  PubKeyHash: 9eee08ab54036069e5aaa15dcb204baa0fae622d
  Check:      d38b9fd2
  Version:    4c
  --------
Pay Address:  XqBBkSnvWMcLyyRRvH1S4mWH4f2zugr7Cd

Implementation Details

It also serves as a reference implementation for porting to other platforms such as modern mobile and desktop programming languages.

As a reference implementation, it's valuable to understand that tedium, here's a peek behind the curtain:

  • Address ↔️ PubKey Hash
  • WIF ↔️ Private Key
  • Private Key ➡️ Public Key
  • Public Key ➡️ PubKey Hash

These are simplified version of what's in the actual code:
(removed error checking, etc, for clarity)

let Base58Check = require("@dashincubator/base58check").Base58Check;

// "dash58check" because we're using the Dash magic version bytes.
let dash58check = Base58Check.create({
  privateKeyVersion: "cc", // "ef" for dash testnet, "80" for bitcoin main
  pubKeyHashVersion: "4c", // "8c" for dash testnet, "00" for bitcoin main
});

/**
 * @param {String} addr
 * @returns {Promise<Uint8Array>} - p2pkh (no magic byte or checksum)
 */
async function addrToPkh(addr) {
  let b58cAddr = dash58check.decode(addr);
  let pubKeyHash = toBytes(b58cAddr.pubKeyHash);
  return pubKeyHash;
}

/**
 * @param {Uint8Array} pubKeyHash - no magic byte or checksum
 * @returns {Promise<String>} - Pay Addr
 */
async function pkhToAddr(pubKeyHash) {
  let hex = toHex(pubKeyHash);
  let addr = await dash58check.encode({ pubKeyHash: hex });
  return addr;
}

/**
 * @param {String} wif
 * @returns {Promise<Uint8Array>} - private key (no magic byte or checksum)
 */
async function wifToPrivKey(wif) {
  let b58cWif = dash58check.decode(wif);
  let privateKey = toBytes(b58cWif.privateKey);
  return privateKey;
}

/**
 * @param {Uint8Array} privKey
 * @returns {Promise<String>} - wif
 */
async function privKeyToWif(privKey) {
  let privateKey = toHex(privKey);

  let wif = await dash58check.encode({ privateKey: privateKey });
  return wif;
}

/**
 * @param {String} addrOrWif
 */
async function decode(addrOrWif) {
  let parts = await dash58check.decode(addrOrWif);
  let check = await dash58check.checksum(parts);
  let valid = parts.check === check;

  parts.valid = valid;
  //parts.privBytes = toBytes(parts.privateKey);
  //parts.shaRipeBytes = toBytes(parts.pubKeyHash);

  return parts;
}

/**
 * @param {Uint8Array} buf
 * @returns {String} - Pay Addr or WIF
 * @throws {Error}
 */
async function encode(buf) {
  let hex = toHex(buf);

  if (32 === buf.length) {
    return await dash58check.encode({
      privateKey: hex,
    });
  }

  if (20 === buf.length) {
    return await dash58check.encode({
      pubKeyHash: hex,
    });
  }

  throw new Error("buffer length must be (PubKeyHash) or 32 (PrivateKey)");
}

LICENSE

To keep the dependency tree slim, this includes BaseX and Base58Check, which are derivatives of base58.cpp, as well as RIPEMD160.

These have all been complete for several years. They do not need updates.

DashKeys.js

Copyright (c) 2022-2023 Dash Incubator
Copyright (c) 2021-2023 AJ ONeal

MIT License

BaseX, Base58, Base58Check

Copyright (c) 2018 base-x contributors
Copyright (c) 2014-2018 The Bitcoin Core developers

MIT License

RIPEMD160

Copyright (c) 2016 crypto-browserify

MIT License

About

Generate, validate, create, and convert WIFs and PayAddress.

Resources

License

Stars

Watchers

Forks

Packages

No packages published