From 51af6339607813a41b66889631b646b474d0263a Mon Sep 17 00:00:00 2001 From: Vin Armani Date: Fri, 15 May 2020 06:42:58 +0000 Subject: [PATCH] threshold module and example --- examples/threshold.js | 116 +++++++++++++++++++++++ index.js | 5 + lib/Transaction.js | 39 ++++++++ lib/threshold/InputScript.js | 50 ++++++++++ lib/threshold/Message.js | 47 +++++++++ lib/threshold/OutputScript.js | 173 ++++++++++++++++++++++++++++++++++ lib/threshold/ScriptNumber.js | 72 ++++++++++++++ lib/threshold/index.js | 3 + package-lock.json | 8 +- package.json | 5 +- 10 files changed, 512 insertions(+), 6 deletions(-) create mode 100644 examples/threshold.js create mode 100644 lib/threshold/InputScript.js create mode 100644 lib/threshold/Message.js create mode 100644 lib/threshold/OutputScript.js create mode 100644 lib/threshold/ScriptNumber.js create mode 100644 lib/threshold/index.js diff --git a/examples/threshold.js b/examples/threshold.js new file mode 100644 index 0000000..9da9cf7 --- /dev/null +++ b/examples/threshold.js @@ -0,0 +1,116 @@ +const jeton = require('../index') +const PrivateKey = jeton.PrivateKey +const ThresholdMessage = jeton.threshold.Message +const OutputScript = jeton.threshold.OutputScript +const Transaction = jeton.Transaction +const Signature = jeton.Signature + +// Create keypairs for 2 players and an oracle +const priv1 = new PrivateKey("KzwmMwHjbmRRdtwVUowKpYmpnJmMaVyGTwYLmh2qmiWcqgd7W9fG") +const pub1 = priv1.toPublicKey() +const priv2 = new PrivateKey("KzyhHmmxwFbv2Mo8bQsJQwXhrCgAtjsCmuqBBmGZrcjfTn1Xvzw1") +const pub2 = priv2.toPublicKey() + +var utxoForPub2 = new Transaction.UnspentOutput({ + txid: + 'b2b671f0cb3d7710b5d8a8420fff14b18173de876da710438dafa0ae6e8f5357', + vout: 1, + satoshis: 10200, + scriptPubKey: '76a9149383fa6588a176c2592cb2f4008d779293246adb88ac' +}) + +var utxoForPub1 = new Transaction.UnspentOutput({ + txid: + 'ee874221a431cf09d3373c4b9ffbb1e8fe80526d4304695e2f97541fc084c8f4', + vout: 1, + satoshis: 10200, + scriptPubKey: '76a914b011100d12d0537232692b3c113be5a8f505395588ac' +}) + +// Create array of UTXO arrays +var splitUtxos = [utxoForPub1, utxoForPub2] + +const oraclePriv = new PrivateKey('L5FDo3MEb2QNs2aQJ5DVGSDE5eBzVsgZny15Ri649RjysWAeLkTs') +const oraclePub = oraclePriv.toPublicKey(); +console.log('user 1 pubkey', pub1.toString()) +console.log('user 2 pubkey', pub2.toString()) +console.log('oracle pubkey', oraclePub.toString()) + +const blockheight = 660211 +const price = 220.2098 + +// This is the ASM for the redeem script if done in CashScript +// 0556 f2120a 029207e74ee73342f9af30859c03f684da444344d957a949c768316519f9df6a36 02c3e42dd2a3806f1bc9a9f32c3a97b872ed03ce8a779242b8bf2dba636ce655b0 OP_6 OP_PICK OP_4 OP_SPLIT OP_DROP OP_BIN2NUM OP_7 OP_PICK OP_4 OP_SPLIT OP_NIP OP_BIN2NUM OP_OVER OP_5 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY OP_SWAP OP_CHECKLOCKTIMEVERIFY OP_DROP OP_3 OP_ROLL OP_GREATERTHANOREQUAL OP_VERIFY OP_3 OP_ROLL OP_4 OP_ROLL OP_3 OP_ROLL OP_CHECKDATASIGVERIFY OP_CHECKSIG + +let message = new ThresholdMessage(blockheight, price).message +console.log('message', message) +let oracleSig = Signature.signCDS(message, oraclePriv) +// console.log('sig DER', oracleSig.toDER()) +let verify = ThresholdMessage.verifySignature(message, oracleSig, oraclePub) +// console.log('signature verified?', verify) + +// Create the output script +var outputScriptData = { + threshold: 218, + blockheight: blockheight, + oraclePubKey: oraclePub, + parties: { + gt: {pubKey: pub1}, + lte: {pubKey: pub2} + } +} + +outScript = new OutputScript(outputScriptData) + +let outputScript = outScript.toScript() + +// console.log(outputScript) + +let outScriptHex = outScript.toScript().toHex() + +// console.log('destination P2SH', outScript.toAddress()) + +// Set miner fee and total amount to send (will be split between UTXOs from useUTXOs array) +var splitUtxoMinerFee = 200 +var amountToSend = 20000 + +var splitTxSendAmount = (amountToSend / splitUtxos.length) + +// Create two separate transactions from players 2 and 3 to fund the escrow +var sighash = (Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID | Signature.SIGHASH_ANYONECANPAY) +var txArray = [] +for (let i = 0; i < splitUtxos.length; i++) { + txArray[i] = new Transaction() + .from(splitUtxos[i]) // Feed information about what unspent outputs one can use + .toP2SH(outScript, amountToSend) + .sign([priv1, priv2], sighash) // Signs all the inputs it can +} + +// Combine the transactions by merging the inputs +var fundTx = Transaction.mergeTransactionInputs(txArray) + +// var itx = new Transaction(fundTx.toString()) + +// console.log('fundTx', itx.toObject()) + +// Now spend the escrow transaction... + +var escrowUtxo = Transaction.utxoFromTxOutput(fundTx, 0) + +// Make Transaction from escrow UTXO +sighash = (Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID) + +var spendTx = new Transaction() +.from(escrowUtxo) +.to(priv1.toAddress(), 19000) +.lockUntilBlockHeight(660211) // Must be after the blockheight in the script + +//console.log(spendTx.toObject()) + +// Sign CDS input at index 0 as player 2 +spendTx.signThreshold(0, priv1, message, oracleSig, outScript.toScript(), sighash) + +// console.log(spendTx.toObject()) +console.log('estimated size', spendTx._estimateSize()) +console.log('verify tx full sig', spendTx.verify()) +console.log('jeton signature verified?', spendTx.verifyScriptSig(0)) \ No newline at end of file diff --git a/index.js b/index.js index df6df15..9257fff 100644 --- a/index.js +++ b/index.js @@ -13,4 +13,9 @@ jeton.escrow = {} jeton.escrow.InputScript = require('./lib/escrow/InputScript') jeton.escrow.OutputScript = require('./lib/escrow/OutputScript') +// Threshold +jeton.threshold = require('./lib/threshold') +//jeton.escrow.InputScript = require('./lib/escrow/InputScript') +//jeton.escrow.OutputScript = require('./lib/escrow/OutputScript') + module.exports = jeton \ No newline at end of file diff --git a/lib/Transaction.js b/lib/Transaction.js index dad4e4e..0c445f2 100644 --- a/lib/Transaction.js +++ b/lib/Transaction.js @@ -7,9 +7,11 @@ const Sighash = Transaction.Sighash const BN = bitcore.crypto.BN const Signature = require('./Signature') const InputScript = require('./escrow/InputScript') +const ThresholdInputScript = require('./threshold/InputScript') Transaction.P2SHFlags = Interpreter.SCRIPT_VERIFY_P2SH + | Interpreter.SCRIPT_VERIFY_CHECKLOCKTIMEVERIFY | Interpreter.SCRIPT_ENABLE_SIGHASH_FORKID | Interpreter.SCRIPT_ENABLE_CHECKDATASIG | Interpreter.SCRIPT_VERIFY_STRICTENC @@ -75,6 +77,43 @@ Transaction.prototype.signEscrow = function (inputIndex, winnerPrivKey, refMsg, } +/** + * Signs an threshold escrow transaction as defined in jeton.threshold.OutputScript + * + * @param {number} inputIndex - index of the input to sign + * @param {PrivateKey} winnerPrivKey - the private key of the escrow beneficiary + * @param {string} oracleMsg - the message for the outcome + * @param {Signature} oracleSig - the signature for the message and referee public key + * @param {Script} subscript - the non-P2SH (original) scriptPubKey + * @param {number} sighash - the type of signature + * + * @returns {Transaction} + */ +Transaction.prototype.signThreshold = function (inputIndex, winnerPrivKey, oracleMsg, oracleSig, subscript, sighash) { + sighash = sighash || (Signature.SIGHASH_ALL | Signature.SIGHASH_FORKID) + + let input = this.toObject()['inputs'][inputIndex] + let p2pkhSig = Sighash.sign(this, winnerPrivKey, sighash, inputIndex, subscript, BN.fromNumber(input.output.satoshis), Transaction.P2SHFlags) + + let winnerScript = Script.buildPublicKeyHashIn(winnerPrivKey.toPublicKey(), p2pkhSig, sighash) + + // Generate scriptSig + let inputScriptData = { + oracleSig: oracleSig, + message: oracleMsg, + winnerScript: winnerScript, + outputScript: subscript + } + + inScript = new ThresholdInputScript(inputScriptData) + + // Set scriptSig for inputIndex + this.inputs[inputIndex].setScript(inScript.toScript()) + + return this +} + + /** * Verify that a P2SH input is properly signed * diff --git a/lib/threshold/InputScript.js b/lib/threshold/InputScript.js new file mode 100644 index 0000000..1d6fde2 --- /dev/null +++ b/lib/threshold/InputScript.js @@ -0,0 +1,50 @@ +const bitcore = require('bitcore-lib-cash') +const Hash = bitcore.crypto.Hash +const Script = bitcore.Script +const PublicKey = bitcore.PublicKey + + +/** + * Instantiate an object to create escrow scriptSig + * + * @param {object} data - The encoded data in various formats + * @param {Signature} data.oracleSig + * @param {Buffer} data.message + * @param {Script} data.winnerScript - a P2PKH scriptSig for the transaction signed by escrow beneficiary + * @param {Script} data.outputScript - The original (non-P2SH) scriptPubKey for this input + * + * @constructor + */ +var InputScript = function (data) { + + this.oracleSig = data.oracleSig + this.message = data.message + this.winnerScript = data.winnerScript + this.outputScript = data.outputScript +} + + +/** + * @returns {Script} + */ +InputScript.prototype.toScript = function () { + let outputBuf = this.outputScript.toBuffer() + let inScript = Script() + .add(this.winnerScript) + .add(this.oracleSig.toBuffer()) + .add(this.message) + .add(outputBuf) + + return inScript +} + + +/** + * @returns {Buffer} + */ +InputScript.prototype.toBuffer = function () { + let outScript = this.toScript() + return outScript.toBuffer() +} + +module.exports = InputScript \ No newline at end of file diff --git a/lib/threshold/Message.js b/lib/threshold/Message.js new file mode 100644 index 0000000..4e1be80 --- /dev/null +++ b/lib/threshold/Message.js @@ -0,0 +1,47 @@ +const bitcore = require('bitcore-lib-cash') +const Address = bitcore.Address +const Hash = bitcore.crypto.Hash +const ECDSA = bitcore.crypto.ECDSA +const Signature = bitcore.crypto.Signature +const Script = bitcore.Script +const PrivateKey = bitcore.PrivateKey +const PublicKey = bitcore.PublicKey +const ScriptNumber = require('./ScriptNumber') + +var Message = function (blockHeight, thresholdValue) { + + this.blockHeight = blockHeight + this.threshold = Math.ceil(thresholdValue) + this.message = this.createMessage() +} + +/** + * Encode a blockHeight and threshold amount into a byte sequence of 8 bytes (4 bytes per value) + * This is compatible with the CashScript PriceOracle.ts + * https://github.com/Bitcoin-com/cashscript/blob/master/examples/PriceOracle.ts + * + * + */ +Message.prototype.createMessage = function () { + const lhs = Buffer.alloc(4, 0); + const rhs = Buffer.alloc(4, 0); + ScriptNumber.encode(this.blockHeight).copy(lhs); + ScriptNumber.encode(this.threshold).copy(rhs); + console.log('decoded price', ScriptNumber.decode(rhs, 4, false)) + console.log('decoded threshold', ScriptNumber.decode(lhs, 4, false)) + return Buffer.concat([lhs, rhs]); +} + +Message.signMessage = function(message, privkey){ + return new ECDSA().set({ + hashbuf: Hash.sha256(message), + privkey: privkey + }).signRandomK().sig; +} + +Message.verifySignature = function(message, sig, pubkey) { + let msgHash = Hash.sha256(message) + return ECDSA.verify(msgHash, sig, pubkey) +} + +module.exports = Message \ No newline at end of file diff --git a/lib/threshold/OutputScript.js b/lib/threshold/OutputScript.js new file mode 100644 index 0000000..6c13267 --- /dev/null +++ b/lib/threshold/OutputScript.js @@ -0,0 +1,173 @@ +const bitcore = require('bitcore-lib-cash') +const Address = bitcore.Address +const Hash = bitcore.crypto.Hash +const Script = bitcore.Script +const PublicKey = bitcore.PublicKey +const Message = require('./Message') +const ScriptNumber = require('./ScriptNumber') + + +/** + * Instantiate an object to create threshold escrow scriptPubKey + * + * @param {object} data - The encoded data in various formats + * @param {PublicKey} data.oraclePubKey + * @param {int} data.threshold + * @param {int} data.blockheight + * @param {object} data.parties - Parties object + * @param {object} data.parties.gt - Object for party predicting "greater than" threshold + * @param {PublicKey} data.parties.gt.pubKey - public key for this party + * @param {Address} data.parties.gt.address - (optional instead of pubKey) P2PKH address for this party + * @param {object} data.parties.lte - Object for party predicting "less than or equal to" + * @param {PublicKey} data.parties.lte.pubKey - public key for this party + * @param {Address} data.parties.lte.address - (optional instead of pubKey) P2PKH address for this party + * + * @constructor + */ +var OutputScript = function (data) { + + this.oraclePubKey = data.oraclePubKey + this.threshold = data.threshold + this.blockheight = data.blockheight + this.parties = data.parties +} + + +/** + * @returns {Script} + */ +OutputScript.prototype.toScript = function () { + //Output Script + let outScript = Script() + + outScript.add('OP_DUP') + outScript.add('OP_4') + outScript.add('OP_SPLIT') + outScript.add('OP_SWAP') + outScript.add('OP_CHECKLOCKTIMEVERIFY') + outScript.add('OP_DROP') + + const threshBuf = Buffer.alloc(4, 0); + ScriptNumber.encode(this.threshold).copy(threshBuf); + outScript.add(threshBuf) + + outScript.add('OP_LESSTHANOREQUAL') + outScript.add('OP_3') + outScript.add('OP_PICK') + outScript.add('OP_HASH160') + outScript.add('OP_SWAP') + + outScript.add('OP_IF') + outScript.add(this.buildPartyConditional('lte')) + outScript.add('OP_ELSE') + outScript.add(this.buildPartyConditional('gt')) + outScript.add('OP_ENDIF') + + outScript.add(this.oraclePubKey.toBuffer()) + + outScript.add('OP_CHECKDATASIGVERIFY') + outScript.add('OP_CHECKSIG') + + return outScript +} + + +/** + * @returns {Buffer} + */ +OutputScript.prototype.toBuffer = function () { + let outScript = this.toScript() + return outScript.toBuffer() +} + + +/** + * Return P2SH version of script + * + * @returns {Script} + */ +OutputScript.prototype.toScriptHash = function () { + let outputBuf = this.toBuffer() + var outputP2SH = new Script() + .add('OP_HASH160') + .add(Hash.sha256ripemd160(outputBuf)) + .add('OP_EQUAL') + + return outputP2SH +} + + +/** + * Return P2SH address + * @param {Network|string=} network - a {@link Network} object, or a string with the network name ('livenet' or 'testnet') + * + * @returns {Address} + */ +OutputScript.prototype.toAddress = function (network) { + network = network || 'livenet' + let address = new Address(this.toScriptHash(), network, 'scripthash') + return address +} + + +/** + * @param {string} gtOrLte = "gt" or "lte" + * + * @returns {Script} + */ +OutputScript.prototype.buildPartyConditional = function(gtOrLte) { + let party = this.parties[gtOrLte] + if(party.address) { + if(typeof(party.address) == 'string') + party.address = Address.fromString(party.address) + if(party.address instanceof Address) + party.pubKey = party.address + } + if(party.pubKey instanceof PublicKey) + party.pubKey = party.pubKey.toAddress() + + let s = Script() + .add(party.pubKey.hashBuffer) + .add('OP_EQUALVERIFY') + + return s +} + + +/** + * Parse scriptPubKey into object reflecting inputs + * @param {string} + * + * @returns {object} + */ +// OutputScript.parseScriptPubKey = function(hexString) { +// let getAllIndexes = function (arr, val) { +// var indexes = [], i; +// for(i = 0; i < arr.length; i++) +// if (arr[i] === val) +// indexes.push(i); +// return indexes; +// } + +// let parsedData = { +// refereePubKey: null, +// parties: [] +// } +// let raw_script = Buffer.from(hexString, 'hex') +// let script = new Script(raw_script) +// let outscriptArr = script.toASM().split(' ') +// let endifAppearances = getAllIndexes(outscriptArr, 'OP_IF') +// // console.log('endifAppearances', endifAppearances) +// for(let i = 0; i < endifAppearances.length; i++) { +// let ifIndex = endifAppearances[i] +// let partyArray = outscriptArr.slice(ifIndex+2, ifIndex+8) +// parsedData.parties[i] = { +// message: Buffer.from(partyArray[0], 'hex').toString(), +// pubKeyHash: partyArray[5] +// } +// parsedData.refereePubKey = partyArray[1] +// } +// return parsedData +// } + +module.exports = OutputScript \ No newline at end of file diff --git a/lib/threshold/ScriptNumber.js b/lib/threshold/ScriptNumber.js new file mode 100644 index 0000000..f49db4e --- /dev/null +++ b/lib/threshold/ScriptNumber.js @@ -0,0 +1,72 @@ +/** + * Taken from https://github.com/Bitcoin-com/bitcoincashjs2-lib/blob/master/src/script_number.js + */ + +var Buffer = require('safe-buffer').Buffer + +function decode (buffer, maxLength, minimal) { + maxLength = maxLength || 4 + minimal = minimal === undefined ? true : minimal + + var length = buffer.length + if (length === 0) return 0 + if (length > maxLength) throw new TypeError('Script number overflow') + if (minimal) { + if ((buffer[length - 1] & 0x7f) === 0) { + if (length <= 1 || (buffer[length - 2] & 0x80) === 0) throw new Error('Non-minimally encoded script number') + } + } + + // 40-bit + if (length === 5) { + var a = buffer.readUInt32LE(0) + var b = buffer.readUInt8(4) + + if (b & 0x80) return -(((b & ~0x80) * 0x100000000) + a) + return (b * 0x100000000) + a + } + + var result = 0 + + // 32-bit / 24-bit / 16-bit / 8-bit + for (var i = 0; i < length; ++i) { + result |= buffer[i] << (8 * i) + } + + if (buffer[length - 1] & 0x80) return -(result & ~(0x80 << (8 * (length - 1)))) + return result +} + +function scriptNumSize (i) { + return i > 0x7fffffff ? 5 + : i > 0x7fffff ? 4 + : i > 0x7fff ? 3 + : i > 0x7f ? 2 + : i > 0x00 ? 1 + : 0 +} + +function encode (number) { + var value = Math.abs(number) + var size = scriptNumSize(value) + var buffer = Buffer.allocUnsafe(size) + var negative = number < 0 + + for (var i = 0; i < size; ++i) { + buffer.writeUInt8(value & 0xff, i) + value >>= 8 + } + + if (buffer[size - 1] & 0x80) { + buffer.writeUInt8(negative ? 0x80 : 0x00, size - 1) + } else if (negative) { + buffer[size - 1] |= 0x80 + } + + return buffer +} + +module.exports = { + decode: decode, + encode: encode +} \ No newline at end of file diff --git a/lib/threshold/index.js b/lib/threshold/index.js new file mode 100644 index 0000000..70d24f9 --- /dev/null +++ b/lib/threshold/index.js @@ -0,0 +1,3 @@ +module.exports.InputScript = require('./InputScript') +module.exports.OutputScript = require('./OutputScript') +module.exports.Message = require('./Message') \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e46a949..b9561d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jeton-lib", - "version": "1.3.5", + "version": "1.4.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -129,9 +129,9 @@ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } } diff --git a/package.json b/package.json index 913d913..3c27c51 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jeton-lib", - "version": "1.4.6", + "version": "1.5.0", "description": "Extension of bitcore-lib-cash for advanced Bitcoin Cash transaction types", "main": "index.js", "scripts": { @@ -9,6 +9,7 @@ "author": "", "license": "MIT", "dependencies": { - "bitcore-lib-cash": "^9.0.0" + "bitcore-lib-cash": "^9.0.0", + "safe-buffer": "^5.2.1" } }