/*!
* public.js - hd public keys for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const base58 = require('bcrypto/lib/encoding/base58');
const sha512 = require('bcrypto/lib/sha512');
const hash160 = require('bcrypto/lib/hash160');
const hash256 = require('bcrypto/lib/hash256');
const cleanse = require('bcrypto/lib/cleanse');
const secp256k1 = require('bcrypto/lib/secp256k1');
const Network = require('../protocol/network');
const consensus = require('../protocol/consensus');
const common = require('./common');
/**
* HDPublicKey
* @alias module:hd.PublicKey
* @property {Number} depth
* @property {Number} parentFingerPrint
* @property {Number} childIndex
* @property {Buffer} chainCode
* @property {Buffer} publicKey
*/
class HDPublicKey extends bio.Struct {
/**
* Create an HD public key.
* @constructor
* @param {Object|Base58String} options
* @param {Base58String?} options.xkey - Serialized base58 key.
* @param {Number?} options.depth
* @param {Number?} options.parentFingerPrint
* @param {Number?} options.childIndex
* @param {Buffer?} options.chainCode
* @param {Buffer?} options.publicKey
*/
constructor(options) {
super();
this.depth = 0;
this.parentFingerPrint = 0;
this.childIndex = 0;
this.chainCode = consensus.ZERO_HASH;
this.publicKey = common.ZERO_KEY;
this.fingerPrint = -1;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @private
* @param {Object} options
*/
fromOptions(options) {
assert(options, 'No options for HDPublicKey');
assert((options.depth & 0xff) === options.depth);
assert((options.parentFingerPrint >>> 0) === options.parentFingerPrint);
assert((options.childIndex >>> 0) === options.childIndex);
assert(Buffer.isBuffer(options.chainCode));
assert(Buffer.isBuffer(options.publicKey));
this.depth = options.depth;
this.parentFingerPrint = options.parentFingerPrint;
this.childIndex = options.childIndex;
this.chainCode = options.chainCode;
this.publicKey = options.publicKey;
return this;
}
/**
* Get HD public key (self).
* @returns {HDPublicKey}
*/
toPublic() {
return this;
}
/**
* Get cached base58 xprivkey (always null here).
* @returns {null}
*/
xprivkey(network) {
return null;
}
/**
* Get cached base58 xpubkey.
* @returns {Base58String}
*/
xpubkey(network) {
return this.toBase58(network);
}
/**
* Destroy the key (zeroes chain code and pubkey).
*/
destroy() {
this.depth = 0;
this.childIndex = 0;
this.parentFingerPrint = 0;
cleanse(this.chainCode);
cleanse(this.publicKey);
this.fingerPrint = -1;
}
/**
* Derive a child key.
* @param {Number} index - Derivation index.
* @param {Boolean?} hardened - Whether the derivation
* should be hardened (throws if true).
* @returns {HDPrivateKey}
* @throws on `hardened`
*/
derive(index, hardened) {
assert(typeof index === 'number');
if ((index >>> 0) !== index)
throw new Error('Index out of range.');
if ((index & common.HARDENED) || hardened)
throw new Error('Cannot derive hardened.');
if (this.depth >= 0xff)
throw new Error('Depth too high.');
const id = this.getID(index);
const cache = common.cache.get(id);
if (cache)
return cache;
const bw = bio.pool(37);
bw.writeBytes(this.publicKey);
bw.writeU32BE(index);
const data = bw.render();
const hash = sha512.mac(data, this.chainCode);
const left = hash.slice(0, 32);
const right = hash.slice(32, 64);
let key;
try {
key = secp256k1.publicKeyTweakAdd(this.publicKey, left, true);
} catch (e) {
return this.derive(index + 1);
}
if (this.fingerPrint === -1) {
const fp = hash160.digest(this.publicKey);
this.fingerPrint = fp.readUInt32BE(0, true);
}
const child = new this.constructor();
child.depth = this.depth + 1;
child.parentFingerPrint = this.fingerPrint;
child.childIndex = index;
child.chainCode = right;
child.publicKey = key;
common.cache.set(id, child);
return child;
}
/**
* Unique HD key ID.
* @private
* @param {Number} index
* @returns {String}
*/
getID(index) {
return 'b' + this.publicKey.toString('hex') + index;
}
/**
* Derive a BIP44 account key (does not derive, only ensures account key).
* @method
* @param {Number} purpose
* @param {Number} type
* @param {Number} account
* @returns {HDPublicKey}
* @throws Error if key is not already an account key.
*/
deriveAccount(purpose, type, account) {
assert((purpose >>> 0) === purpose);
assert((type >>> 0) === type);
assert((account >>> 0) === account);
assert(this.isAccount(account), 'Cannot derive account index.');
return this;
}
/**
* Test whether the key is a master key.
* @method
* @returns {Boolean}
*/
isMaster() {
return common.isMaster(this);
}
/**
* Test whether the key is (most likely) a BIP44 account key.
* @method
* @param {Number?} account
* @returns {Boolean}
*/
isAccount(account) {
return common.isAccount(this, account);
}
/**
* Test whether a string is a valid path.
* @param {String} path
* @param {Boolean?} hardened
* @returns {Boolean}
*/
static isValidPath(path) {
try {
common.parsePath(path, false);
return true;
} catch (e) {
return false;
}
}
/**
* Derive a key from a derivation path.
* @param {String} path
* @returns {HDPublicKey}
* @throws Error if `path` is not a valid path.
* @throws Error if hardened.
*/
derivePath(path) {
const indexes = common.parsePath(path, false);
let key = this;
for (const index of indexes)
key = key.derive(index);
return key;
}
/**
* Compare a key against an object.
* @param {Object} obj
* @returns {Boolean}
*/
equals(obj) {
assert(HDPublicKey.isHDPublicKey(obj));
return this.depth === obj.depth
&& this.parentFingerPrint === obj.parentFingerPrint
&& this.childIndex === obj.childIndex
&& this.chainCode.equals(obj.chainCode)
&& this.publicKey.equals(obj.publicKey);
}
/**
* Compare a key against an object.
* @param {Object} obj
* @returns {Boolean}
*/
compare(key) {
assert(HDPublicKey.isHDPublicKey(key));
let cmp = this.depth - key.depth;
if (cmp !== 0)
return cmp;
cmp = this.parentFingerPrint - key.parentFingerPrint;
if (cmp !== 0)
return cmp;
cmp = this.childIndex - key.childIndex;
if (cmp !== 0)
return cmp;
cmp = this.chainCode.compare(key.chainCode);
if (cmp !== 0)
return cmp;
cmp = this.publicKey.compare(key.publicKey);
if (cmp !== 0)
return cmp;
return 0;
}
/**
* Convert key to a more json-friendly object.
* @returns {Object}
*/
getJSON(network) {
return {
xpubkey: this.xpubkey(network)
};
}
/**
* Inject properties from json object.
* @private
* @param {Object} json
* @param {Network?} network
*/
fromJSON(json, network) {
assert(json.xpubkey, 'Could not handle HD key JSON.');
this.fromBase58(json.xpubkey, network);
return this;
}
/**
* Test whether an object is in the form of a base58 xpubkey.
* @param {String} data
* @param {(Network|NetworkType)?} network
* @returns {Boolean}
*/
static isBase58(data, network) {
if (typeof data !== 'string')
return false;
if (data.length < 4)
return false;
const prefix = data.substring(0, 4);
try {
Network.fromPublic58(prefix, network);
return true;
} catch (e) {
return false;
}
}
/**
* Test whether a buffer has a valid network prefix.
* @param {Buffer} data
* @param {(Network|NetworkType)?} network
* @returns {NetworkType}
*/
static isRaw(data, network) {
if (!Buffer.isBuffer(data))
return false;
if (data.length < 4)
return false;
const version = data.readUInt32BE(0, true);
try {
Network.fromPublic(version, network);
return true;
} catch (e) {
return false;
}
}
/**
* Inject properties from a base58 key.
* @private
* @param {Base58String} xkey
* @param {Network?} network
*/
fromBase58(xkey, network) {
assert(typeof xkey === 'string');
return this.decode(base58.decode(xkey), network);
}
/**
* Inject properties from serialized data.
* @private
* @param {BufferReader} br
* @param {(Network|NetworkType)?} network
*/
read(br, network) {
const version = br.readU32BE();
Network.fromPublic(version, network);
this.depth = br.readU8();
this.parentFingerPrint = br.readU32BE();
this.childIndex = br.readU32BE();
this.chainCode = br.readBytes(32);
this.publicKey = br.readBytes(33);
br.verifyChecksum(hash256.digest);
return this;
}
/**
* Serialize key data to base58 extended key.
* @param {(Network|NetworkType)?} network
* @returns {Base58String}
*/
toBase58(network) {
return base58.encode(this.encode(network));
}
/**
* Write the key to a buffer writer.
* @param {BufferWriter} bw
* @param {(Network|NetworkType)?} network
*/
write(bw, network) {
network = Network.get(network);
bw.writeU32BE(network.keyPrefix.xpubkey);
bw.writeU8(this.depth);
bw.writeU32BE(this.parentFingerPrint);
bw.writeU32BE(this.childIndex);
bw.writeBytes(this.chainCode);
bw.writeBytes(this.publicKey);
bw.writeChecksum(hash256.digest);
return bw;
}
/**
* Calculate serialization size.
* @returns {Number}
*/
getSize() {
return 82;
}
/**
* Instantiate an HD public key from a base58 string.
* @param {Base58String} xkey
* @param {Network?} network
* @returns {HDPublicKey}
*/
static fromBase58(xkey, network) {
return new this().fromBase58(xkey, network);
}
/**
* Test whether an object is a HDPublicKey.
* @param {Object} obj
* @returns {Boolean}
*/
static isHDPublicKey(obj) {
return obj instanceof HDPublicKey;
}
}
/*
* Expose
*/
module.exports = HDPublicKey;