Skip to content

Commit

Permalink
implement script builder
Browse files Browse the repository at this point in the history
  • Loading branch information
kajoseph committed Sep 12, 2023
1 parent 14170f3 commit 38c1071
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 35 deletions.
78 changes: 58 additions & 20 deletions packages/bitcore-lib/lib/publickey.js
Original file line number Diff line number Diff line change
Expand Up @@ -296,15 +296,31 @@ PublicKey.fromX = function(odd, x) {
});
};

PublicKey.fromTaproot = function(buf) {
$.checkArgument(_.isBuffer(buf));
$.checkArgument(buf.length === 32, 'Taproot public keys must be 32 bytes');
return new PublicKey.fromX(false, buf);
/**
* 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), 'input must be a hex string or buffer');
$.checkArgument(hexBuf.length === 32, 'Taproot public keys must be 32 bytes');
return new PublicKey.fromX(false, hexBuf);
}

PublicKey.isValidTaproot = function(buf) {
$.checkArgument(_.isBuffer(buf));
$.checkArgument(buf.length === 32, 'Taproot public keys must be 32 bytes');
/**
* Verifies if the input is a valid Taproot public key
* @param {String|Buffer} hexBuf
* @returns {Boolean}
*/
PublicKey.isValidTaproot = function(hexBuf) {
if (typeof hexBuf === 'string' && JSUtil.isHexaString(hexBuf)) {
hexBuf = Buffer.from(hexBuf, 'hex');
}
$.checkArgument(Buffer.isBuffer(hexBuf), 'input must be a hex string or buffer');
$.checkArgument(hexBuf.length === 32, 'Taproot public keys must be 32 bytes');

// TODO: do a more thorough taproot validation

Expand All @@ -321,16 +337,20 @@ PublicKey.isValidTaproot = function(buf) {
};


PublicKey.prototype.computeTapTweakHash = function(p, merkleRoot) {
$.checkArgument(p instanceof PublicKey);
/**
* 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(p.point.x.toBuffer({ size: 32 }));
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(_.isBuffer(merkleRoot) && merkleRoot.length === 32, 'merkleRoot must be 32 byte buffer');
$.checkArgument(Buffer.isBuffer(merkleRoot) && merkleRoot.length === 32, 'merkleRoot must be 32 byte buffer');
taggedWriter.write(merkleRoot);
}
const tweakHash = taggedWriter.finalize();
Expand All @@ -340,8 +360,19 @@ PublicKey.prototype.computeTapTweakHash = function(p, merkleRoot) {
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) {
const tweak = this.computeTapTweakHash(p, merkleRoot);
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)));
Expand All @@ -355,18 +386,25 @@ PublicKey.prototype.checkTapTweak = function(p, merkleRoot, control) {
return true;
}


/**
* Create a tweaked version of this pub key
* @param {Buffer} merkleRoot (optional)
* @returns {Buffer}
*/
PublicKey.prototype.createTapTweak = function(merkleRoot) {
$.checkArgument(_.isBuffer(merkleRoot) && merkleRoot.length === 32, 'merkleRoot must be a 32 byte buffer');
$.checkArgument(merkleRoot == null || (Buffer.isBuffer(merkleRoot) && merkleRoot.length === 32), 'merkleRoot must be a 32 byte buffer');

// TODO

// secp256k1_xonly_pubkey base_point;
// if (!secp256k1_xonly_pubkey_parse(secp256k1_context_verify, &base_point, data())) return std::nullopt;
// secp256k1_pubkey out;
const tweak = this.computeTapTweakHash(merkleRoot);
// if (!secp256k1_xonly_pubkey_tweak_add(secp256k1_context_verify, &out, &base_point, tweak.data())) return std::nullopt;
// const ret = secp256k1_xonly_pubkey_serialize(secp256k1_context_verify, ret.first.begin(), &out_xonly);
return ret;
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()
};
}

/**
Expand Down
59 changes: 46 additions & 13 deletions packages/bitcore-lib/lib/script/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ var Networks = require('../networks');
var $ = require('../util/preconditions');
var _ = require('lodash');
var errors = require('../errors');
var buffer = require('buffer');
var BufferUtil = require('../util/buffer');
var JSUtil = require('../util/js');
const TaggedHash = require('../crypto/taggedhash');

/**
* A bitcoin transaction script. Each transaction's inputs and outputs
Expand Down Expand Up @@ -935,23 +935,54 @@ Script.buildWitnessV0Out = function(to) {

/**
* Build Taproot script output
* @param {PublicKey} to
* @param {PublicKey} pubKey recipient's pubKey
* @param {Array|Object} scriptTree single leaf object OR array of leaves. leaf: { script: String, leafVersion: Integer }
* @returns {Script}
*/
Script.buildWitnessV1Out = function(to) {
$.checkArgument(!_.isUndefined(to));
$.checkArgument(to instanceof PublicKey || to instanceof Address || _.isString(to));
Script.buildWitnessV1Out = function(pubKey, scriptTree) {
$.checkArgument(pubKey instanceof PublicKey || pubKey instanceof Address || typeof pubKey === 'string');
$.checkArgument(!scriptTree || Array.isArray(scriptTree) || !!scriptTree.script);

if (typeof pubKey === 'string') {
pubKey = PublicKey.fromTaproot(pubKey);
}

if (to instanceof PublicKey) {
to = to.toAddress(null, Address.PayToWitnessPublicKeyHash);
} else if (_.isString(to)) {
to = new Address(to);
function buildTree(tree) {
if (Array.isArray(tree)) {
const [left, leftH] = buildTree(tree[0]);
const [right, rightH] = buildTree(tree[1]);
const ret = [[[left[0], left[1]], rightH], [[right[0], right[1]], leftH]];
const hWriter = TaggedHash.TAPBRANCH;
if (leftH.compare(rightH) === 1) {
hWriter.write(rightH);
hWriter.write(leftH);
} else {
hWriter.write(leftH);
hWriter.write(rightH);
}
return [ret, hWriter.finalize()];
} else {
const { leafVersion, script } = tree;
const scriptBuf = new Script(script).toBuffer();
const leafWriter = TaggedHash.TAPLEAF;
leafWriter.writeUInt8(leafVersion);
leafWriter.writeUInt8(scriptBuf.length);
leafWriter.write(scriptBuf);
const h = leafWriter.finalize();
return [[Buffer.from([leafVersion]), scriptBuf], h];
}
}

let taggedHash = null;
if (scriptTree) {
const [_, h] = buildTree(scriptTree);
taggedHash = h;
}

var s = new Script();
s.add(Opcode.OP_1)
.add(to.hashBuffer);
s._network = to.network;
const { tweakedPubKey } = pubKey.createTapTweak(taggedHash);
const s = new Script();
s.add(Opcode.OP_1);
s.add(tweakedPubKey);
return s;
};

Expand Down Expand Up @@ -1073,6 +1104,8 @@ Script.fromAddress = function(address) {
return Script.buildWitnessV0Out(address);
} else if (address.isPayToWitnessScriptHash()) {
return Script.buildWitnessV0Out(address);
} else if (address.isPayToTaproot()) {
return Script.buildWitnessV1Out(address);
}
throw new errors.Script.UnrecognizedAddress(address);
};
Expand Down
16 changes: 14 additions & 2 deletions packages/bitcore-lib/test/script/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

var should = require('chai').should();
var expect = require('chai').expect;
var testVectors = require('../data/bitcoind/wallet_test_vectors.json');
throw new Error('TODO: write tests for wallet_test_vectors.json');
const taprootTestVectors = require('../data/bitcoind/wallet_test_vectors.json');
var bitcore = require('../..');

var BufferUtil = bitcore.util.buffer;
Expand Down Expand Up @@ -1078,4 +1077,17 @@ describe('Script', function() {
trueCount.should.equal(defaultCount);
});
});

describe('Taproot', function() {
describe('scriptPubKey', function() {
for (let i = 0; i < taprootTestVectors.scriptPubKey.length; i++) {
const vec = taprootTestVectors.scriptPubKey[i];
it(`vector ${i}: ${vec.given.internalPubkey} -> ${vec.expected.bip350Address}`, function() {
const script = Script.buildWitnessV1Out(vec.given.internalPubkey, vec.given.scriptTree);
script.toAddress().toString().should.equal(vec.expected.bip350Address);
throw new Error('TODO: do intermediary validation');
});
}
});
});
});

0 comments on commit 38c1071

Please sign in to comment.