bitcore-lib
Version:
A pure and powerful JavaScript Bitcoin library.
500 lines (447 loc) • 14.4 kB
JavaScript
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;
;