diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 778f593..cdaecc3 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -13,9 +13,20 @@ var Transaction = require('./transaction') // inspects a scriptSig w/ optional redeemScript and // derives any input information required -function expandInput (scriptSig, redeemScript) { - var scriptSigChunks = bscript.decompile(scriptSig) - var prevOutType = bscript.classifyInput(scriptSigChunks, true) +function expandInput (scriptSig, redeemScript, witnessStack) { + var witnessType + if (witnessStack) { + witnessType = bscript.classifyWitness(witnessStack) + } + + var prevOutType, scriptSigChunks + if (scriptSig.length === 0 && witnessStack) { + prevOutType = witnessType + } else { + scriptSigChunks = bscript.decompile(scriptSig) + prevOutType = bscript.classifyInput(scriptSigChunks, true) + } + var pubKeys, signatures, prevOutScript switch (prevOutType) { @@ -31,8 +42,14 @@ function expandInput (scriptSig, redeemScript) { result.redeemScriptType = result.prevOutType result.prevOutScript = bscript.scriptHash.output.encode(bcrypto.hash160(redeemScript)) result.prevOutType = scriptTypes.P2SH + result.witness = false return result + case scriptTypes.P2WPKH: + pubKeys = witnessStack.slice(1) + signatures = witnessStack.slice(0, 1) + break + case scriptTypes.P2PKH: // if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)') pubKeys = scriptSigChunks.slice(1) @@ -59,13 +76,19 @@ function expandInput (scriptSig, redeemScript) { return chunk === ops.OP_0 ? undefined : chunk }) break + + case scriptTypes.NONSTANDARD: + return { prevOutType: prevOutType, prevOutScript: EMPTY_SCRIPT } + + default: return {} } return { pubKeys: pubKeys, signatures: signatures, prevOutScript: prevOutScript, - prevOutType: prevOutType + prevOutType: prevOutType, + witness: Boolean(witnessStack) } } @@ -123,6 +146,15 @@ function expandOutput (script, scriptType, ourPubKey) { if (pkh1.equals(pkh2)) pubKeys = [ourPubKey] break + // does our hash160(pubKey) match the output scripts? + case scriptTypes.P2WPKH: + if (!ourPubKey) break + + var wpkh1 = scriptChunks[1] + var wpkh2 = bcrypto.hash160(ourPubKey) + if (wpkh1.equals(wpkh2)) pubKeys = [ourPubKey] + break + case scriptTypes.P2PK: pubKeys = scriptChunks.slice(0, 1) break @@ -163,11 +195,15 @@ function prepareInput (input, kpPubKey, redeemScript) { input.redeemScriptType = expanded.scriptType input.prevOutScript = input.prevOutScript || bscript.scriptHash.output.encode(redeemScriptHash) input.prevOutType = scriptTypes.P2SH + input.witness = false // maybe we have some prevOut knowledge } else if (input.prevOutType) { - // pay-to-scriptHash is not possible without a redeemScript - if (input.prevOutType === scriptTypes.P2SH) throw new Error('PrevOutScript is P2SH, missing redeemScript') + // embedded scripts are not possible without a redeemScript + if (input.prevOutType === scriptTypes.P2SH || + input.prevOutType === scriptTypes.P2WSH) { + throw new Error('PrevOutScript is ' + input.prevOutType + ', requires redeemScript') + } // try to derive missing information using our kpPubKey expanded = expandOutput(input.prevOutScript, input.prevOutType, kpPubKey) @@ -175,6 +211,7 @@ function prepareInput (input, kpPubKey, redeemScript) { input.pubKeys = expanded.pubKeys input.signatures = expanded.signatures + input.witness = (input.prevOutScript === scriptTypes.P2WPKH) // no prior knowledge, assume pubKeyHash } else { @@ -182,24 +219,31 @@ function prepareInput (input, kpPubKey, redeemScript) { input.prevOutType = scriptTypes.P2PKH input.pubKeys = [kpPubKey] input.signatures = [undefined] + input.witness = false } } +var EMPTY_SCRIPT = new Buffer(0) + function buildInput (input, allowIncomplete) { var signatures = input.signatures var scriptType = input.redeemScriptType || input.prevOutType - var scriptSig + var stack switch (scriptType) { + case scriptTypes.P2WPKH: + if (signatures.length < 1 || !signatures[0]) throw new Error('Not enough signatures provided') + stack = bscript.witnessPubKeyHash.input.encodeStack(signatures[0], input.pubKeys[0]) + break + case scriptTypes.P2PKH: + if (signatures.length < 1 || !signatures[0]) throw new Error('Not enough signatures provided') + stack = bscript.pubKeyHash.input.encodeStack(signatures[0], input.pubKeys[0]) + break + case scriptTypes.P2PK: if (signatures.length < 1 || !signatures[0]) throw new Error('Not enough signatures provided') - if (scriptType === scriptTypes.P2PKH) { - scriptSig = bscript.pubKeyHash.input.encode(signatures[0], input.pubKeys[0]) - } else { - scriptSig = bscript.pubKey.input.encode(signatures[0]) - } - + stack = bscript.pubKey.input.encodeStack(signatures[0]) break // ref https://github.com/bitcoin/bitcoin/blob/d612837814020ae832499d18e6ee5eb919a87907/src/script/sign.cpp#L232 @@ -213,18 +257,40 @@ function buildInput (input, allowIncomplete) { signatures = signatures.filter(function (x) { return x !== ops.OP_0 }) } - scriptSig = bscript.multisig.input.encode(signatures, allowIncomplete ? undefined : input.redeemScript) + stack = bscript.multisig.input.encodeStack(signatures, allowIncomplete ? undefined : input.redeemScript) break default: return } - // wrap as scriptHash if necessary - if (input.prevOutType === scriptTypes.P2SH) { - scriptSig = bscript.scriptHash.input.encode(scriptSig, input.redeemScript) + var script, witness + + // encode witness for P2WPKH if necessary + if (input.prevOutType === scriptTypes.P2WPKH || + input.redeemScriptType === scriptTypes.P2WPKH) { + witness = stack + script = EMPTY_SCRIPT } - return scriptSig + // no witness? plain old script! + if (witness === undefined) { + script = bscript.compilePushOnly(stack) + } + + // wrap as scriptHash if necessary + if (input.prevOutType === scriptTypes.P2SH) { + script = bscript.scriptHash.input.encode(script, input.redeemScript) + } + + // falsy is easier + if (script === EMPTY_SCRIPT) { + script = undefined + } + + return { + script: script, + witness: witness + } } function TransactionBuilder (network, maximumFeeRate) { @@ -276,7 +342,8 @@ TransactionBuilder.fromTransaction = function (transaction, network) { transaction.ins.forEach(function (txIn) { txb.__addInputUnsafe(txIn.hash, txIn.index, { sequence: txIn.sequence, - script: txIn.script + script: txIn.script, + witness: txIn.witness }) }) @@ -328,7 +395,7 @@ TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, options) // derive what we can from the scriptSig if (options.script !== undefined) { - input = expandInput(options.script) + input = expandInput(options.script, null, options.witness) } // if an input value was given, retain it @@ -395,16 +462,14 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { var scriptType = input.redeemScriptType || input.prevOutType if (!scriptType && !allowIncomplete) throw new Error('Transaction is not complete') - // build a scriptSig - var scriptSig = buildInput(input, allowIncomplete) + var result = buildInput(input, allowIncomplete) - // skip if no scriptSig exists - if (!scriptSig) { - if (!allowIncomplete) throw new Error(scriptType + ' not supported') - return - } + // skip if no result + if (!result && allowIncomplete) return + if (result && result.script) return tx.setInputScript(i, result.script) + if (result && result.witness) return tx.setWitness(i, result.witness) - tx.setInputScript(i, scriptSig) + throw new Error(scriptType + ' not supported') }) if (!allowIncomplete) { @@ -422,10 +487,11 @@ function canSign (input) { input.pubKeys !== undefined && input.signatures !== undefined && input.signatures.length === input.pubKeys.length && - input.pubKeys.length > 0 + input.pubKeys.length > 0 && + input.witness !== undefined } -TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashType) { +TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashType, witnessValue) { if (keyPair.network !== this.network) throw new Error('Inconsistent network') if (!this.inputs[vin]) throw new Error('No input at index: ' + vin) hashType = hashType || Transaction.SIGHASH_ALL @@ -441,14 +507,20 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy var kpPubKey = keyPair.getPublicKeyBuffer() if (!canSign(input)) { - prepareInput(input, kpPubKey, redeemScript) + prepareInput(input, kpPubKey, redeemScript, witnessValue) if (!canSign(input)) throw Error(input.prevOutType + ' not supported') } // ready to sign var hashScript = input.redeemScript || input.prevOutScript - var signatureHash = this.tx.hashForSignature(vin, hashScript, hashType) + + var signatureHash + if (input.witness) { + signatureHash = this.tx.hashForWitnessV0(vin, hashScript, witnessValue, hashType) + } else { + signatureHash = this.tx.hashForSignature(vin, hashScript, hashType) + } // enforce in order signing of public keys var signed = input.pubKeys.some(function (pubKey, i) { diff --git a/test/fixtures/transaction_builder.json b/test/fixtures/transaction_builder.json index 811e998..855703d 100644 --- a/test/fixtures/transaction_builder.json +++ b/test/fixtures/transaction_builder.json @@ -377,6 +377,29 @@ "value": 10000 } ] + }, + { + "description": "Transaction w/ P2WPKH -> P2WPKH", + "txHex": "01000000000101ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff011027000000000000160014aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5024730440220092de398bfbd808f6a2315c184cebf2f872692853482781eaeb72057706ac31202200a666c0eba2da011d04499b071143124559cad32a063785d713f2e0d4a15945101210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000", + "inputs": [ + { + "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "vout": 0, + "prevTxScript": "OP_0 751e76e8199196d454941c45d1b3a323f1433bd6", + "signs": [ + { + "keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn", + "value": 10000 + } + ] + } + ], + "outputs": [ + { + "script": "OP_0 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5", + "value": 10000 + } + ] } ], "fromTransaction": [ @@ -852,7 +875,7 @@ ] }, { - "exception": "PrevOutScript is P2SH, missing redeemScript", + "exception": "PrevOutScript is scripthash, requires redeemScript", "inputs": [ { "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", diff --git a/test/transaction_builder.js b/test/transaction_builder.js index a33c075..fbe3315 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -283,10 +283,10 @@ describe('TransactionBuilder', function () { } if (!sign.throws) { - txb.sign(index, keyPair, redeemScript, sign.hashType) + txb.sign(index, keyPair, redeemScript, sign.hashType, sign.value) } else { assert.throws(function () { - txb.sign(index, keyPair, redeemScript, sign.hashType) + txb.sign(index, keyPair, redeemScript, sign.hashType, sign.value) }, new RegExp(f.exception)) } })