diff --git a/src/transaction_builder.js b/src/transaction_builder.js index dc0cddb..30eba5a 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -14,9 +14,139 @@ var ECPair = require('./ecpair') var ECSignature = require('./ecsignature') var Transaction = require('./transaction') +function extractChunks (type, chunks) { + var pubKeys = [] + var signatures = [] + switch (type) { + case scriptTypes.P2PKH: + // if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)') + pubKeys = chunks.slice(1) + signatures = chunks.slice(0, 1) + break + + case scriptTypes.P2PK: + pubKeys[0] = null + signatures = chunks.slice(0, 1) + break + + case scriptTypes.MULTISIG: + signatures = chunks.slice(1).map(function (chunk) { + return chunk === ops.OP_0 ? undefined : chunk + }) + break + } + return { + pubKeys: pubKeys, + signatures: signatures + } +} + +function expandInput (scriptSig, redeemScript, witnessStack) { + var prevOutScript + var prevOutType + var witnessScript + var witnessScriptType + var witness = false + var p2wsh = false + var p2sh = false + var witnessProgram + + var classifyWitness = bscript.classifyWitness(witnessStack); + if (classifyWitness === scriptTypes.P2WSH) { + witnessScript = witnessStack[witnessStack.length - 1] + witnessScriptType = bscript.classifyOutput(witnessScript) + p2wsh = true + if (scriptSig.length === 0) { + prevOutScript = bscript.witnessScriptHash.output.encode(bcrypto.sha256(witnessScript)) + // bare witness + } else { + if (!redeemScript) { + throw new Error('No redeemScript provided for P2WSH, but scriptSig wasn\'t empty') + } + witnessProgram = bscript.witnessScriptHash.output.encode(bcrypto.sha256(witnessScript)) + if (!redeemScript.equals(witnessProgram)) { + throw new Error('Redeem script didn\'t match witnessScript') + } + prevOutScript = bscript.scriptHash.output.encode(bscript.hash160(witnessProgram)) + } + + console.log(bscript.classifyOutput(witnessScript)) + console.log(SIGNABLE.indexOf(bscript.classifyOutput(witnessScript))) + if (SIGNABLE.indexOf(bscript.classifyOutput(witnessScript)) === -1) { + throw new Error('unsupported witness script') + } + } else if (classifyWitness === scriptTypes.P2WPKH) { + var keyHash = witnessStack[witnessStack.length - 1] + if (scriptSig.length === 0) { + prevOutScript = bscript.witnessPubKeyHash.output.encode(keyHash) + // bare witness + } else { + if (!redeemScript) { + throw new Error('No redeemScript provided for P2WPKH, but scriptSig wasn\'t empty'); + } + witnessProgram = bscript.witnessPubKeyHash.output.encode(keyHash) + if (!redeemScript.equals(witnessProgram)) { + throw new Error('Redeem script did not have the right witness program') + } + prevOutScript = bscript.scriptHash.output.encode(bcrypto.hash160(witnessProgram)); + } + } + + if (typeof prevOutScript === 'undefined' && redeemScript) { + prevOutScript = bscript.scriptHash.output.encode(bcrypto.hash160(redeemScript)) + } + + if (typeof prevOutScript === 'undefined' && scriptSig) { + prevOutType = bscript.classifyInput(scriptSig) + if (!(scriptTypes.P2SH === prevOutType || P2SH.indexOf(prevOutType) !== -1)) { + throw new Error('Unsupported scriptSig') + } + } + + var scriptType = bscript.classifyOutput(prevOutScript) + var redeemScriptType + var chunks = bscript.toStack(scriptSig) + if (scriptType === scriptTypes.P2SH) { + p2sh = true + scriptType = redeemScriptType = bscript.classifyOutput(redeemScript) + if (P2SH.indexOf(scriptType) === -1) { + throw new Error('P2SH script not supported ' + scriptType) + } + chunks = chunks.slice(0, -1) + } + + if (scriptType === scriptTypes.P2WSH) { + chunks = witnessStack.slice(0, -1) + scriptType = bscript.classifyOutput(witnessScript) + } else if (scriptType === scriptTypes.P2WPKH) { + chunks = witnessStack + } + + var expanded = extractChunks(scriptType, chunks) + + var result = { + pubKeys: expanded.pubKeys, + signatures: expanded.signatures, + prevOutScript: prevOutScript, + prevOutType: prevOutType + } + + if (p2sh) { + result.redeemScript = redeemScript + result.redeemScriptType = redeemScriptType + } + + if (p2wsh) { + result.witnessScript = witnessScript + result.witnessScriptType = witnessScriptType + } + + return result +} + // inspects a scriptSig w/ optional redeemScript and // derives any input information required -function expandInput (scriptSig, redeemScript, witnessStack) { +function expandInput2 (scriptSig, redeemScript, witnessStack) { var witnessType if (witnessStack) { witnessType = bscript.classifyWitness(witnessStack) @@ -53,33 +183,6 @@ function expandInput (scriptSig, redeemScript, witnessStack) { signatures = witnessStack.slice(0, 1) break - case scriptTypes.P2PKH: - // if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)') - pubKeys = scriptSigChunks.slice(1) - signatures = scriptSigChunks.slice(0, 1) - - if (redeemScript) break - prevOutScript = bscript.pubKeyHash.output.encode(bcrypto.hash160(pubKeys[0])) - break - - case scriptTypes.P2PK: - if (redeemScript) { - pubKeys = bscript.decompile(redeemScript).slice(0, 1) - } - - signatures = scriptSigChunks.slice(0, 1) - break - - case scriptTypes.MULTISIG: - if (redeemScript) { - pubKeys = bscript.decompile(redeemScript).slice(1, -2) - } - - signatures = scriptSigChunks.slice(1).map(function (chunk) { - return chunk === ops.OP_0 ? undefined : chunk - }) - break - case scriptTypes.NONSTANDARD: return { prevOutType: prevOutType, prevOutScript: EMPTY_SCRIPT } @@ -190,7 +293,7 @@ function checkP2WSHInput (input, witnessScriptHash) { if (input.prevOutType !== scriptTypes.P2WSH) throw new Error('PrevOutScript must be P2WSH') var scriptHash = bscript.decompile(input.prevOutScript)[1] - if (!scriptHash.equals(witnessScriptHash)) throw new Error('Inconsistent hash160(WitnessScript)') + if (!scriptHash.equals(witnessScriptHash)) throw new Error('Inconsistent sha25(WitnessScript)') } } @@ -235,7 +338,7 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip p2sh = true p2shType = expanded.scriptType } else if (witnessScript) { - witnessScriptHash = bcrypto.hash256(witnessScript) + witnessScriptHash = bcrypto.sha256(witnessScript) checkP2WSHInput(input, witnessScriptHash) expanded = expandOutput(witnessScript, undefined, kpPubKey) @@ -259,7 +362,7 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip witness = (input.prevOutScript === scriptTypes.P2WPKH) } else { - prevOutScript = bscript.pubKeyHash.output.encode(bcrypto.hash160(kpPubKey)) + prevOutScript = bscript.witnessPubKeyHash.output.encode(bcrypto.hash160(kpPubKey)) expanded = expandOutput(prevOutScript, scriptTypes.P2PKH, kpPubKey) prevOutType = scriptTypes.P2PKH witness = false @@ -402,11 +505,13 @@ TransactionBuilder.fromTransaction = function (transaction, network) { // Copy inputs transaction.ins.forEach(function (txIn) { + console.log('add input') txb.__addInputUnsafe(txIn.hash, txIn.index, { sequence: txIn.sequence, script: txIn.script, witness: txIn.witness }) + console.log('done input') }) // fix some things not possible through the public API @@ -457,6 +562,7 @@ TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, options) // derive what we can from the scriptSig if (options.script !== undefined) { + console.log('options.script provided, so peek') input = expandInput(options.script, null, options.witness) } @@ -485,6 +591,7 @@ TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, options) } var vin = this.tx.addInput(txHash, vout, options.sequence, options.scriptSig) + console.log(this.tx) this.inputs[vin] = input this.prevTxMap[prevTxOut] = vin @@ -520,7 +627,9 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { var tx = this.tx.clone() // Create script signatures from inputs this.inputs.forEach(function (input, i) { - var scriptType = input.redeemScriptType || input.prevOutType + console.log(input) + var scriptType = input.witnessScriptType || input.redeemScriptType || input.prevOutType + console.log(scriptType) if (!scriptType && !allowIncomplete) throw new Error('Transaction is not complete') var result = buildInput(input, allowIncomplete) @@ -576,11 +685,12 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy } // ready to sign - var hashScript = input.redeemScript || input.prevOutScript + var hashScript = input.witnessScript || input.redeemScript || input.prevOutScript var signatureHash if (input.witness) { signatureHash = this.tx.hashForWitnessV0(vin, hashScript, witnessValue, hashType) + console.log(hashScript); } else { signatureHash = this.tx.hashForSignature(vin, hashScript, hashType) } diff --git a/test/fixtures/transaction_builder.json b/test/fixtures/transaction_builder.json index 855703d..c6bcead 100644 --- a/test/fixtures/transaction_builder.json +++ b/test/fixtures/transaction_builder.json @@ -400,7 +400,33 @@ "value": 10000 } ] + }, + + { + "description": "Transaction w/ P2WSH P2PK -> P2PKH", + "txHex": "010000000001014533a3bc1e039bd787656068e135aaee10aee95a64776bfc047ee6a7c1ebdd2f0000000000ffffffff0160ea0000000000001976a914851a33a5ef0d4279bd5854949174e2c65b1d450088ac02473044022039725bb7291a14dd182dafdeaf3ea0d5c05c34f4617ccbaa46522ca913995c4e02203b170d072ed2e489e7424ad96d8fa888deb530be2d4c5d9aaddf111a7efdb2d3012321038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2bac00000000", + "inputs": [ + { + "txId": "2fddebc1a7e67e04fc6b77645ae9ae10eeaa35e168606587d79b031ebca33345", + "vout": 0, + "prevTxScript": "OP_0 0f9ea7bae7166c980169059e39443ed13324495b0d6678ce716262e879591210", + "signs": [ + { + "keyPair": "L2FroWqrUgsPpTMhpXcAFnVDLPTToDbveh3bhDaU4jhe7Cw6YujN", + "witnessScript": "038de63cf582d058a399a176825c045672d5ff8ea25b64d28d4375dcdb14c02b2b OP_CHECKSIG", + "value": 80000 + } + ] + } + ], + "outputs": [ + { + "script": "OP_DUP OP_HASH160 851a33a5ef0d4279bd5854949174e2c65b1d4500 OP_EQUALVERIFY OP_CHECKSIG", + "value": 60000 + } + ] } + ], "fromTransaction": [ { diff --git a/test/transaction_builder.js b/test/transaction_builder.js index fbe3315..655bcc6 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -55,12 +55,19 @@ function construct (f, dontSign) { input.signs.forEach(function (sign) { var keyPair = ECPair.fromWIF(sign.keyPair, network) var redeemScript - + var witnessScript + var value if (sign.redeemScript) { redeemScript = bscript.fromASM(sign.redeemScript) } + if (sign.value) { + value = sign.value + } + if (sign.witnessScript) { + witnessScript = bscript.fromASM(sign.witnessScript) + } - txb.sign(index, keyPair, redeemScript, sign.hashType) + txb.sign(index, keyPair, redeemScript, sign.hashType, value, witnessScript) }) }) @@ -82,6 +89,7 @@ describe('TransactionBuilder', function () { fixtures.valid.build.forEach(function (f) { it('returns TransactionBuilder, with ' + f.description, function () { var network = NETWORKS[f.network || 'bitcoin'] + var tx = Transaction.fromHex(f.txHex) var txb = TransactionBuilder.fromTransaction(tx, network)