UNPKG

bitcore-lib

Version:

A pure and powerful JavaScript Bitcoin library.

500 lines (447 loc) 14.4 kB
'use strict'; var BN = require('./crypto/bn'); var Point = require('./crypto/point'); var Hash = require('./crypto/hash'); var JSUtil = require('./util/js'); var Network = require('./networks'); var _ = require('lodash'); var $ = require('./util/preconditions'); const TaggedHash = require('./crypto/taggedhash'); /** * Instantiate a PublicKey from a {@link PrivateKey}, {@link Point}, `string`, or `Buffer`. * * There are two internal properties, `network` and `compressed`, that deal with importing * a PublicKey from a PrivateKey in WIF format. More details described on {@link PrivateKey} * * @example * ```javascript * // instantiate from a private key * var key = PublicKey(privateKey, true); * * // export to as a DER hex encoded string * var exported = key.toString(); * * // import the public key * var imported = PublicKey.fromString(exported); * ``` * * @param {string} data - The encoded data in various formats * @param {Object} extra - additional options * @param {Network=} extra.network - Which network should the address for this public key be for * @param {String=} extra.compressed - If the public key is compressed * @returns {PublicKey} A new valid instance of an PublicKey * @constructor */ function PublicKey(data, extra) { if (!(this instanceof PublicKey)) { return new PublicKey(data, extra); } $.checkArgument(data, 'First argument is required, please include public key data.'); if (data instanceof PublicKey) { // Return copy, but as it's an immutable object, return same argument return data; } extra = extra || {}; var info = this._classifyArgs(data, extra); // validation info.point.validate(); JSUtil.defineImmutable(this, { point: info.point, compressed: info.compressed, network: info.network || Network.defaultNetwork }); return this; }; /** * Internal function to differentiate between arguments passed to the constructor * @param {*} data * @param {Object} extra */ PublicKey.prototype._classifyArgs = function(data, extra) { /* jshint maxcomplexity: 10 */ var info = { compressed: _.isUndefined(extra.compressed) || extra.compressed }; // detect type of data if (data instanceof Point) { info.point = data; } else if (data.x && data.y) { info = PublicKey._transformObject(data); } else if (typeof(data) === 'string') { info = PublicKey._transformDER(Buffer.from(data, 'hex')); } else if (PublicKey._isBuffer(data)) { info = PublicKey._transformDER(data); } else if (PublicKey._isPrivateKey(data)) { info = PublicKey._transformPrivateKey(data); } else { throw new TypeError('First argument is an unrecognized data format.'); } if (!info.network) { info.network = _.isUndefined(extra.network) ? undefined : Network.get(extra.network); } return info; }; /** * Internal function to detect if an object is a {@link PrivateKey} * * @param {*} param - object to test * @returns {boolean} * @private */ PublicKey._isPrivateKey = function(param) { var PrivateKey = require('./privatekey'); return param instanceof PrivateKey; }; /** * Internal function to detect if an object is a Buffer * * @param {*} param - object to test * @returns {boolean} * @private */ PublicKey._isBuffer = function(param) { return (param instanceof Buffer) || (param instanceof Uint8Array); }; /** * Internal function to transform a private key into a public key point * * @param {PrivateKey} privkey - An instance of PrivateKey * @returns {Object} An object with keys: point and compressed * @private */ PublicKey._transformPrivateKey = function(privkey) { $.checkArgument(PublicKey._isPrivateKey(privkey), 'Must be an instance of PrivateKey'); var info = {}; info.point = Point.getG().mul(privkey.bn); info.compressed = privkey.compressed; info.network = privkey.network; return info; }; /** * Internal function to transform DER into a public key point * * @param {Buffer} buf - An hex encoded buffer * @param {bool=} strict - if set to false, will loosen some conditions * @returns {Object} An object with keys: point and compressed * @private */ PublicKey._transformDER = function(buf, strict) { /* jshint maxstatements: 30 */ /* jshint maxcomplexity: 12 */ $.checkArgument(PublicKey._isBuffer(buf), 'Must be a hex buffer of DER encoded public key'); var info = {}; strict = _.isUndefined(strict) ? true : strict; var x; var y; var xbuf; var ybuf; if (buf[0] === 0x04 || (!strict && (buf[0] === 0x06 || buf[0] === 0x07))) { xbuf = buf.slice(1, 33); ybuf = buf.slice(33, 65); if (xbuf.length !== 32 || ybuf.length !== 32 || buf.length !== 65) { throw new TypeError('Length of x and y must be 32 bytes'); } x = new BN(xbuf); y = new BN(ybuf); info.point = new Point(x, y); info.compressed = false; } else if (buf[0] === 0x03) { xbuf = buf.slice(1); x = new BN(xbuf); info = PublicKey._transformX(true, x); info.compressed = true; } else if (buf[0] === 0x02) { xbuf = buf.slice(1); x = new BN(xbuf); info = PublicKey._transformX(false, x); info.compressed = true; } else { throw new TypeError('Invalid DER format public key'); } return info; }; /** * Internal function to transform X into a public key point * * @param {Boolean} odd - If the point is above or below the x axis * @param {Point} x - The x point * @returns {Object} An object with keys: point and compressed * @private */ PublicKey._transformX = function(odd, x) { $.checkArgument(typeof odd === 'boolean', 'Must specify whether y is odd or not (true or false)'); var info = {}; info.point = Point.fromX(odd, x); return info; }; /** * Internal function to transform a JSON into a public key point * * @param {String|Object} json - a JSON string or plain object * @returns {Object} An object with keys: point and compressed * @private */ PublicKey._transformObject = function(json) { var x = new BN(json.x, 'hex'); var y = new BN(json.y, 'hex'); var point = new Point(x, y); return new PublicKey(point, { compressed: json.compressed }); }; /** * Instantiate a PublicKey from a PrivateKey * * @param {PrivateKey} privkey - An instance of PrivateKey * @returns {PublicKey} A new valid instance of PublicKey */ PublicKey.fromPrivateKey = function(privkey) { $.checkArgument(PublicKey._isPrivateKey(privkey), 'Must be an instance of PrivateKey'); var info = PublicKey._transformPrivateKey(privkey); return new PublicKey(info.point, { compressed: info.compressed, network: info.network }); }; /** * Instantiate a PublicKey from a Buffer * @param {Buffer} buf A DER buffer (33+ bytes) or a 32 byte X-only coordinate (taproot only) * @param {Boolean} strict (optional; Only applies to DER format) If set to false, will loosen some conditions * @returns {PublicKey} */ PublicKey.fromBuffer = function(buf, strict) { $.checkArgument(PublicKey._isBuffer(buf), 'Must be a hex buffer of DER encoded public key or 32 byte X coordinate (taproot)'); if (buf.length === 32) { return PublicKey.fromX(false, buf); } return PublicKey.fromDER(buf, strict); } /** * Instantiate a PublicKey from a DER buffer * @param {Buffer} buf - A DER hex buffer * @param {bool=} strict - if set to false, will loosen some conditions * @returns {PublicKey} A new valid instance of PublicKey */ PublicKey.fromDER = function(buf, strict) { $.checkArgument(PublicKey._isBuffer(buf), 'Must be a hex buffer of DER encoded public key'); var info = PublicKey._transformDER(buf, strict); return new PublicKey(info.point, { compressed: info.compressed }); }; /** * Instantiate a PublicKey from a Point * * @param {Point} point - A Point instance * @param {boolean=} compressed - whether to store this public key as compressed format * @returns {PublicKey} A new valid instance of PublicKey */ PublicKey.fromPoint = function(point, compressed) { $.checkArgument(point instanceof Point, 'First argument must be an instance of Point.'); return new PublicKey(point, { compressed: compressed }); }; /** * Instantiate a PublicKey from a DER hex encoded string * * @param {string} str - A DER hex string * @param {String=} encoding - The type of string encoding * @returns {PublicKey} A new valid instance of PublicKey */ PublicKey.fromString = function(str, encoding) { var buf = Buffer.from(str, encoding || 'hex'); var info = PublicKey._transformDER(buf); return new PublicKey(info.point, { compressed: info.compressed }); }; /** * Instantiate a PublicKey from an X Point * * @param {Boolean} odd - If the point is above or below the x axis * @param {Point} x - The x point * @returns {PublicKey} A new valid instance of PublicKey */ PublicKey.fromX = function(odd, x) { var info = PublicKey._transformX(odd, x); return new PublicKey(info.point, { compressed: info.compressed }); }; /** * PublicKey instance from a Taproot (32-byte) public key * @param {String|Buffer} hexBuf * @returns {PublicKey} */ PublicKey.fromTaproot = function(hexBuf) { if (typeof hexBuf === 'string' && JSUtil.isHexaString(hexBuf)) { hexBuf = Buffer.from(hexBuf, 'hex'); } $.checkArgument(Buffer.isBuffer(hexBuf), 'hexBuf must be a hex string or buffer'); $.checkArgument(hexBuf.length === 32, 'Taproot public keys must be 32 bytes'); return new PublicKey.fromX(false, hexBuf); } /** * Verifies if the input is a valid Taproot public key * @param {String|Buffer} hexBuf * @returns {Boolean} */ PublicKey.isValidTaproot = function(hexBuf) { try { return !!PublicKey.fromTaproot(hexBuf); } catch (e) { return false; } }; /** * Get the TapTweak tagged hash of this pub key and the merkleRoot * @param {Buffer} merkleRoot (optional) * @returns {Buffer} */ PublicKey.prototype.computeTapTweakHash = function(merkleRoot) { const taggedWriter = new TaggedHash('TapTweak'); taggedWriter.write(this.point.x.toBuffer({ size: 32 })); // If !merkleRoot, then we have no scripts. The actual tweak does not matter, but // follow BIP341 here to allow for reproducible tweaking. if (merkleRoot) { $.checkArgument(Buffer.isBuffer(merkleRoot) && merkleRoot.length === 32, 'merkleRoot must be 32 byte buffer'); taggedWriter.write(merkleRoot); } const tweakHash = taggedWriter.finalize(); const order = Point.getN(); $.checkState(BN.fromBuffer(tweakHash).lt(order), 'TapTweak hash failed secp256k1 order check'); return tweakHash; }; /** * Verify a tweaked public key against this key * @param {PublicKey|Buffer} p Tweaked pub key * @param {Buffer} merkleRoot (optional) * @param {Buffer} control * @returns {Boolean} */ PublicKey.prototype.checkTapTweak = function(p, merkleRoot, control) { if (Buffer.isBuffer(p)) { p = PublicKey.fromTaproot(p); } const tweak = p.computeTapTweakHash(merkleRoot); const P = p.point.liftX(); const Q = P.add(this.point.curve.g.mul(BN.fromBuffer(tweak))); return this.point.x.eq(Q.x) && Q.y.mod(new BN(2)).eq(new BN(control[0] & 1)); }; /** * Create a tweaked version of this pub key * @param {Buffer} merkleRoot (optional) * @returns {{ parity: Number, tweakedPubKey: Buffer }} */ PublicKey.prototype.createTapTweak = function(merkleRoot) { $.checkArgument(merkleRoot == null || (Buffer.isBuffer(merkleRoot) && merkleRoot.length === 32), 'merkleRoot must be a 32 byte buffer'); let t = this.computeTapTweakHash(merkleRoot); t = new BN(t); const Q = this.point.liftX().add(Point.getG().mul(t)); const parity = Q.y.isEven() ? 0 : 1; return { parity, tweakedPubKey: Q.x.toBuffer() }; }; /** * Check if there would be any errors when initializing a PublicKey * * @param {string} data - The encoded data in various formats * @returns {null|Error} An error if exists */ PublicKey.getValidationError = function(data) { var error; try { /* jshint nonew: false */ new PublicKey(data); } catch (e) { error = e; } return error; }; /** * Check if the parameters are valid * * @param {string} data - The encoded data in various formats * @returns {Boolean} If the public key would be valid */ PublicKey.isValid = function(data) { return !PublicKey.getValidationError(data); }; /** * @returns {Object} A plain object of the PublicKey */ PublicKey.prototype.toObject = PublicKey.prototype.toJSON = function toObject() { return { x: this.point.getX().toString('hex', 2), y: this.point.getY().toString('hex', 2), compressed: this.compressed }; }; /** * Will output the PublicKey to a DER Buffer * * @returns {Buffer} A DER hex encoded buffer */ PublicKey.prototype.toBuffer = PublicKey.prototype.toDER = function() { var x = this.point.getX(); var y = this.point.getY(); var xbuf = x.toBuffer({ size: 32 }); var ybuf = y.toBuffer({ size: 32 }); var prefix; if (!this.compressed) { prefix = Buffer.from([0x04]); return Buffer.concat([prefix, xbuf, ybuf]); } else { var odd = ybuf[ybuf.length - 1] % 2; if (odd) { prefix = Buffer.from([0x03]); } else { prefix = Buffer.from([0x02]); } return Buffer.concat([prefix, xbuf]); } }; /** * Will return a sha256 + ripemd160 hash of the serialized public key * @see https://github.com/bitcoin/bitcoin/blob/master/src/pubkey.h#L141 * @returns {Buffer} */ PublicKey.prototype._getID = function _getID() { return Hash.sha256ripemd160(this.toBuffer()); }; /** * Will return an address for the public key * * @param {String|Network=} network - Which network should the address be for * @param {string} type - Either 'pubkeyhash', 'witnesspubkeyhash', or 'scripthash' * @returns {Address} An address generated from the public key */ PublicKey.prototype.toAddress = function(network, type) { var Address = require('./address'); return Address.fromPublicKey(this, network || this.network, type); }; /** * Will output the PublicKey to a DER encoded hex string * * @returns {string} A DER hex encoded string */ PublicKey.prototype.toString = function() { return this.toDER().toString('hex'); }; /** * Will return a string formatted for the console * * @returns {string} Public key */ PublicKey.prototype.inspect = function() { return '<PublicKey: ' + this.toString() + (this.compressed ? '' : ', uncompressed') + '>'; }; module.exports = PublicKey;