Skip to content

Commit

Permalink
Merge branch 'taproot' of github.com:kajoseph/bitcore
Browse files Browse the repository at this point in the history
  • Loading branch information
kajoseph committed Jun 20, 2024
2 parents 4ac8b10 + 5f061d7 commit c072586
Show file tree
Hide file tree
Showing 38 changed files with 180,565 additions and 444 deletions.
2 changes: 2 additions & 0 deletions packages/bitcore-lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ global._bitcore = bitcore.version;
bitcore.crypto = {};
bitcore.crypto.BN = require('./lib/crypto/bn');
bitcore.crypto.ECDSA = require('./lib/crypto/ecdsa');
bitcore.crypto.Schnorr = require('./lib/crypto/schnorr');
bitcore.crypto.Hash = require('./lib/crypto/hash');
bitcore.crypto.Random = require('./lib/crypto/random');
bitcore.crypto.Point = require('./lib/crypto/point');
bitcore.crypto.Signature = require('./lib/crypto/signature');
bitcore.crypto.TaggedHash = require('./lib/crypto/taggedhash');

// encoding
bitcore.encoding = {};
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcore-lib/lib/address.js
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ Address._transformPublicKey = function(pubkey, network, type) {
if (type === Address.PayToScriptHash) {
info.hashBuffer = Hash.sha256ripemd160(Script.buildWitnessV0Out(pubkey).toBuffer());
} else if (type === Address.PayToTaproot) {
info.hashBuffer = Hash.sha256ripemd160(Script.buildWitnessV1Out(pubkey).toBuffer());
info.hashBuffer = pubkey.createTapTweak().tweakedPubKey;
} else {
info.hashBuffer = Hash.sha256ripemd160(pubkey.toBuffer());
}
Expand Down
33 changes: 32 additions & 1 deletion packages/bitcore-lib/lib/crypto/point.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,22 @@ Point.getG = function getG() {
/**
*
* Will return the max of range of valid private keys as governed by the secp256k1 ECDSA standard.
*
* (A.K.A curve order)
* @link https://en.bitcoin.it/wiki/Private_key#Range_of_valid_ECDSA_private_keys
* @returns {BN} A BN instance of the number of points on the curve
*/
Point.getN = function getN() {
return new BN(ec.curve.n.toArray());
};

/**
* Secp256k1 field size
* @returns {BN} A BN instance of the field size
*/
Point.getP = function() {
return ec.curve.p.clone();
};

Point.prototype._getX = Point.prototype.getX;

/**
Expand Down Expand Up @@ -147,4 +155,27 @@ Point.pointToCompressed = function pointToCompressed(point) {
return BufferUtil.concat([prefix, xbuf]);
};


Point.prototype.liftX = function() {
const fieldSize = Point.getP();
const zero = new BN(0);
const one = new BN(1);
const two = new BN(2);
const three = new BN(3);
const four = new BN(4);
const seven = new BN(7);
const red = BN.red('k256');

const c = this.x.pow(three).add(seven).mod(fieldSize);
const y = c.toRed(red).redPow(fieldSize.add(one).div(four)).mod(fieldSize);

if (!c.eq(y.pow(two).mod(fieldSize))) {
throw new Error('liftX failed');
}

const pointX = this.x.red ? this.x.fromRed() : this.x;
const pointY = y.mod(two).eq(zero) ? y.fromRed() : fieldSize.sub(y)
return new Point(pointX, pointY, true);
};

module.exports = Point;
142 changes: 142 additions & 0 deletions packages/bitcore-lib/lib/crypto/schnorr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
const crypto = require('crypto');
const $ = require('../util/preconditions');
const JS = require('../util/js');
const BN = require('./bn');
const Point = require('./point');
const TaggedHash = require('./taggedhash');

const Schnorr = function Schnorr() {
if (!(this instanceof Schnorr)) {
return new Schnorr();
}
return this;
};

Schnorr.prototype.set = function() {};

/**
* Create a schnorr signature
* @param {PrivateKey|Buffer|BN} privateKey
* @param {String|Buffer} message Hex string or buffer
* @param {String|Buffer} aux Hex string or buffer
* @returns {Buffer}
* @link https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#Default_Signing
*/
Schnorr.sign = function(privateKey, message, aux) {
privateKey = Buffer.isBuffer(privateKey) ? privateKey : privateKey.toBuffer();
if (privateKey.length !== 32) {
throw new Error('Private key should be 32 bytes for schnorr signatures');
}

if (typeof message === 'string') {
$.checkArgument(JS.isHexaString(message), 'Schnorr message string is not hex');
message = Buffer.from(message, 'hex')
}
$.checkArgument($.isType(message, 'Buffer'), 'Schnorr message must be a hex string or buffer');

if (!aux) {
aux = crypto.randomBytes(32);
}
if (typeof aux === 'string') {
$.checkArgument(JS.isHexaString(aux), 'Schnorr aux string is not hex');
aux = Buffer.from(aux, 'hex')
}
$.checkArgument($.isType(aux, 'Buffer'), 'Schnorr aux must be a hex string or buffer');

const G = Point.getG();
const n = Point.getN();

const dPrime = new BN(privateKey);
if (dPrime.eqn(0) || dPrime.gte(n)) {
throw new Error('Invalid private key for schnorr signing');
}
const P = G.mul(dPrime);
const Pbuf = Buffer.from(P.encodeCompressed().slice(1)); // slice(1) removes the encoding prefix byte
const d = P.y.isEven() ? dPrime : n.sub(dPrime);
const t = d.xor(new BN(new TaggedHash('BIP0340/aux', aux).finalize()));
const rand = new TaggedHash('BIP0340/nonce', Buffer.concat([t.toBuffer(), Pbuf, message])).finalize();
const kPrime = new BN(rand).mod(n);
if (kPrime.eqn(0)) {
throw new Error('Error creating schnorr signature');
}
const R = G.mul(kPrime);
const Rbuf = Buffer.from(R.encodeCompressed().slice(1)); // slice(1) removes the encoding prefix byte
const k = R.y.isEven() ? kPrime : n.sub(kPrime);
const e = new BN(new TaggedHash('BIP0340/challenge', Buffer.concat([Rbuf, Pbuf, message])).finalize()).mod(n);
const sig = Buffer.concat([Rbuf, k.add(e.mul(d)).mod(n).toBuffer()]);

if (!Schnorr.verify(Pbuf, message, sig)) {
throw new Error('Error creating schnorr signature. Verification failed');
}
return sig;
};


/**
* Verify a schnorr signature
* @param {PublicKey|Buffer} publicKey
* @param {String|Buffer} message Hex string or buffer
* @param {String|Signature|Buffer} signature Hex string, Signature instance, or buffer
* @returns {Boolean}
* @link https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#Verification
*/
Schnorr.verify = function(publicKey, message, signature) {
if ($.isType(publicKey, 'PublicKey')) {
publicKey = publicKey.point.x.toBuffer();
}
if (publicKey.length !== 32) {
throw new Error('Public key should be 32 bytes for schnorr signatures');
}

if (typeof message === 'string') {
$.checkArgument(JS.isHexaString(message), 'Schnorr message string is not hex');
message = Buffer.from(message, 'hex');
}
if (message.length !== 32) {
throw new Error('Message should be a 32 byte buffer');
}

if (typeof signature === 'string') {
$.checkArgument(JS.isHexaString(signature), 'Schnorr signature string is not hex');
signature = Buffer.from(signature, 'hex');
}
if (typeof signature.toBuffer === 'function') {
signature = signature.toBuffer();
if (signature.length === 65) {
signature = signature.slice(0, 64); // remove the sighashType byte
}
}
if (signature.length !== 64) {
throw new Error('Signature should be a 64 byte buffer');
}

try {
const p = Point.getP();
const n = Point.getN();

const P = Point.fromX(false, publicKey).liftX();
const r = new BN(signature.slice(0, 32));
const s = new BN(signature.slice(32, 64));
if (r.gte(p) || s.gte(n)) {
return false;
}
const e = getE(r, P, message);
const G = Point.getG();
const R = G.mul(s).add(P.mul(e).neg());
if (R.inf || !R.y.isEven() || !R.x.eq(r)) {
return false;
}
return true;
} catch (e) {
return false;
}
};

/* Utility function used in Verify() */
const getE = function(r, P, message) {
const n = Point.getN();
const hash = new TaggedHash('BIP0340/challenge', Buffer.concat([r.toBuffer({ size: 32 }), P.x.toBuffer({ size: 32 }), message])).finalize();
return new BN(hash).mod(n);
};

module.exports = Schnorr;
76 changes: 65 additions & 11 deletions packages/bitcore-lib/lib/crypto/signature.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ var $ = require('../util/preconditions');
var BufferUtil = require('../util/buffer');
var JSUtil = require('../util/js');

var Signature = function Signature(r, s) {
var Signature = function Signature(r, s, isSchnorr) {
if (!(this instanceof Signature)) {
return new Signature(r, s);
return new Signature(r, s, isSchnorr);
}
if (r instanceof BN) {
this.set({
r: r,
s: s
s: s,
isSchnorr: isSchnorr
});
} else if (r) {
var obj = r;
Expand All @@ -26,9 +27,11 @@ Signature.prototype.set = function(obj) {
this.r = obj.r || this.r || undefined;
this.s = obj.s || this.s || undefined;

this.i = typeof obj.i !== 'undefined' ? obj.i : this.i; //public key recovery parameter in range [0, 3]
this.compressed = typeof obj.compressed !== 'undefined' ?
obj.compressed : this.compressed; //whether the recovered pubkey is compressed
// public key recovery parameter in range [0, 3]
this.i = typeof obj.i === 'undefined' ? this.i : obj.i;
// whether the recovered pubkey is compressed
this.compressed = typeof obj.compressed === 'undefined' ? this.compressed : obj.compressed;
this.isSchnorr = typeof obj.isSchnorr === 'undefined' ? this.isSchnorr : obj.isSchnorr;
this.nhashtype = obj.nhashtype || this.nhashtype || undefined;
return this;
};
Expand Down Expand Up @@ -61,9 +64,18 @@ Signature.fromCompact = function(buf) {
};

Signature.fromDER = Signature.fromBuffer = function(buf, strict) {
var obj = Signature.parseDER(buf, strict);
var sig = new Signature();

// Schnorr Signatures use 65 byte for in tx r [len] 32 , s [len] 32, nhashtype
// NOTE: this check is not very reliable. You should use .fromSchnorr directly if you know it's a schnorr sig.
if((buf.length === 64 || buf.length === 65) && buf[0] != 0x30) {
return Signature.fromSchnorr(buf);
}

$.checkArgument(!(buf.length === 64 && buf[0] === 0x30), new Error('64 DER (ecdsa) signatures not allowed'));

var obj = Signature.parseDER(buf, strict);

sig.r = obj.r;
sig.s = obj.s;

Expand Down Expand Up @@ -165,7 +177,15 @@ Signature.prototype.toCompact = function(i, compressed) {
return Buffer.concat([b1, b2, b3]);
};

/**
* Returns either a DER encoded buffer or a Schnorr encoded buffer if isSchnor == true
*/
Signature.prototype.toBuffer = Signature.prototype.toDER = function() {
if(this.isSchnorr) {
const hashTypeBuf = !this.nhashtype || this.nhashtype === Signature.SIGHASH_DEFAULT ? Buffer.alloc(0) : Buffer.from([this.nhashtype]);
return Buffer.concat([this.r.toBuffer({ size: 32 }), this.s.toBuffer({ size: 32 }), hashTypeBuf]);
}

var rnbuf = this.r.toBuffer();
var snbuf = this.s.toBuffer();

Expand Down Expand Up @@ -305,9 +325,43 @@ Signature.prototype.toTxFormat = function() {
return Buffer.concat([derbuf, buf]);
};

Signature.SIGHASH_ALL = 0x01;
Signature.SIGHASH_NONE = 0x02;
Signature.SIGHASH_SINGLE = 0x03;
Signature.SIGHASH_ANYONECANPAY = 0x80;
/**
* Creates a Signature instance from a Schnorr sig
* @param {Buffer} buf Schnorr signature buffer
* @returns {Signature}
*/
Signature.fromSchnorr = function(buf) {
$.checkArgument(Buffer.isBuffer(buf), 'Schnorr signature argument must be a buffer');
$.checkArgument(buf.length === 64 || buf.length === 65, 'Schnorr signatures must be 64 or 65 bytes');

const sig = new Signature();
let r = buf.slice(0,32);
let s = buf.slice(32, 64);
if (buf.length === 65) {
sig.nhashtype = buf[buf.length - 1];
$.checkState(sig.nhashtype !== Signature.SIGHASH_DEFAULT, new Error('invalid hashtype'));
} else {
sig.nhashtype = Signature.SIGHASH_DEFAULT;
}
sig.r = BN.fromBuffer(r);
sig.s = BN.fromBuffer(s);
sig.isSchnorr = true;
return sig;
};

Signature.SIGHASH_DEFAULT = 0x00; //!< Taproot only; implied when sighash byte is missing, and equivalent to SIGHASH_ALL
Signature.SIGHASH_ALL = 0x01;
Signature.SIGHASH_NONE = 0x02;
Signature.SIGHASH_SINGLE = 0x03;
Signature.SIGHASH_ANYONECANPAY = 0x80;

Signature.SIGHASH_OUTPUT_MASK = 3;
Signature.SIGHASH_INPUT_MASK = 128; // 0x80,

Signature.Version = {};
Signature.Version.BASE = 0;
Signature.Version.WITNESS_V0 = 1;
Signature.Version.TAPROOT = 2;
Signature.Version.TAPSCRIPT = 3;

module.exports = Signature;
52 changes: 52 additions & 0 deletions packages/bitcore-lib/lib/crypto/taggedhash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const Hash = require('./hash');
const BufferWriter = require('../encoding/bufferwriter');
const inherits = require('inherits');

/**
* Creates a tag hash to ensure uniqueness of a message between purposes.
* For example, if there's a potential for a collision of messages between
* multiple purposes, a tag can be added to guard against such collisions.
* @link https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki#Design (see 'Tagged Hashes')
* @param {String} tag The tag to prevent message collisions. Should uniquely reflect the purpose of the message.
* @param {Buffer|String} message (optional)
* @param {String} messageEncoding (default: 'hex') If `message` is a string, provide the encoding
* @returns {TaggedHash} Instance of a BufferWriter with the written tag and `finalize` method
*/
function TaggedHash(tag, message, messageEncoding = 'hex') {
if (!(this instanceof TaggedHash)) {
return new TaggedHash(tag, message, messageEncoding);
}
BufferWriter.apply(this);
tag = Buffer.from(tag);

const taghash = Hash.sha256(tag);
this.write(taghash);
this.write(taghash);
if (message) {
message = Buffer.isBuffer(message) ? message : Buffer.from(message, messageEncoding);
this.write(message);
}
return this;
};

inherits(TaggedHash, BufferWriter);

/**
* Returns a 32-byte SHA256 hash of the double tagged hashes concat'd with the message
* as defined by BIP-340: SHA256(SHA256(tag), SHA256(tag), message)
* @returns {Buffer}
*/
TaggedHash.prototype.finalize = function() {
return Buffer.from(Hash.sha256(this.toBuffer()));
};

/**
* Commonly used tags
*/
Object.defineProperties(TaggedHash, {
TAPSIGHASH: { get: () => new TaggedHash('TapSighash') },
TAPLEAF: { get: () => new TaggedHash('TapLeaf') },
TAPBRANCH: { get: () => new TaggedHash('TapBranch') }
});

module.exports = TaggedHash;
Loading

0 comments on commit c072586

Please sign in to comment.