From f4a83f8aed550d250d5dc0822efdf49b67f1ecf6 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 27 Sep 2017 07:05:02 +1000 Subject: [PATCH 1/3] address/txbuilder: require templates to prevent undefined exports --- src/address.js | 17 +++++---- src/transaction_builder.js | 75 +++++++++++++++++++------------------ test/transaction_builder.js | 3 +- 3 files changed, 49 insertions(+), 46 deletions(-) diff --git a/src/address.js b/src/address.js index 508b6ba..221f85a 100644 --- a/src/address.js +++ b/src/address.js @@ -2,6 +2,7 @@ var Buffer = require('safe-buffer').Buffer var bech32 = require('bech32') var bs58check = require('bs58check') var bscript = require('./script') +var btemplates = require('./templates') var networks = require('./networks') var typeforce = require('typeforce') var types = require('./types') @@ -50,10 +51,10 @@ function toBech32 (data, version, prefix) { function fromOutputScript (outputScript, network) { network = network || networks.bitcoin - if (bscript.pubKeyHash.output.check(outputScript)) return toBase58Check(bscript.compile(outputScript).slice(3, 23), network.pubKeyHash) - if (bscript.scriptHash.output.check(outputScript)) return toBase58Check(bscript.compile(outputScript).slice(2, 22), network.scriptHash) - if (bscript.witnessPubKeyHash.output.check(outputScript)) return toBech32(bscript.compile(outputScript).slice(2, 22), 0, network.bech32) - if (bscript.witnessScriptHash.output.check(outputScript)) return toBech32(bscript.compile(outputScript).slice(2, 34), 0, network.bech32) + if (btemplates.pubKeyHash.output.check(outputScript)) return toBase58Check(bscript.compile(outputScript).slice(3, 23), network.pubKeyHash) + if (btemplates.scriptHash.output.check(outputScript)) return toBase58Check(bscript.compile(outputScript).slice(2, 22), network.scriptHash) + if (btemplates.witnessPubKeyHash.output.check(outputScript)) return toBech32(bscript.compile(outputScript).slice(2, 22), 0, network.bech32) + if (btemplates.witnessScriptHash.output.check(outputScript)) return toBech32(bscript.compile(outputScript).slice(2, 34), 0, network.bech32) throw new Error(bscript.toASM(outputScript) + ' has no matching Address') } @@ -67,8 +68,8 @@ function toOutputScript (address, network) { } catch (e) {} if (decode) { - if (decode.version === network.pubKeyHash) return bscript.pubKeyHash.output.encode(decode.hash) - if (decode.version === network.scriptHash) return bscript.scriptHash.output.encode(decode.hash) + if (decode.version === network.pubKeyHash) return btemplates.pubKeyHash.output.encode(decode.hash) + if (decode.version === network.scriptHash) return btemplates.scriptHash.output.encode(decode.hash) } else { try { decode = fromBech32(address) @@ -77,8 +78,8 @@ function toOutputScript (address, network) { if (decode) { if (decode.prefix !== network.bech32) throw new Error(address + ' has an invalid prefix') if (decode.version === 0) { - if (decode.data.length === 20) return bscript.witnessPubKeyHash.output.encode(decode.data) - if (decode.data.length === 32) return bscript.witnessScriptHash.output.encode(decode.data) + if (decode.data.length === 20) return btemplates.witnessPubKeyHash.output.encode(decode.data) + if (decode.data.length === 32) return btemplates.witnessScriptHash.output.encode(decode.data) } } } diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 94d3037..90e5e65 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -2,13 +2,14 @@ var Buffer = require('safe-buffer').Buffer var baddress = require('./address') var bcrypto = require('./crypto') var bscript = require('./script') +var btemplates = require('./templates') var networks = require('./networks') var ops = require('bitcoin-ops') var typeforce = require('typeforce') var types = require('./types') -var scriptTypes = bscript.types -var SIGNABLE = [bscript.types.P2PKH, bscript.types.P2PK, bscript.types.MULTISIG] -var P2SH = SIGNABLE.concat([bscript.types.P2WPKH, bscript.types.P2WSH]) +var scriptTypes = btemplates.types +var SIGNABLE = [btemplates.types.P2PKH, btemplates.types.P2PK, btemplates.types.MULTISIG] +var P2SH = SIGNABLE.concat([btemplates.types.P2WPKH, btemplates.types.P2WSH]) var ECPair = require('./ecpair') var ECSignature = require('./ecsignature') @@ -33,13 +34,13 @@ function extractChunks (type, chunks, script) { break case scriptTypes.P2PK: - pubKeys[0] = script ? bscript.pubKey.output.decode(script) : undefined + pubKeys[0] = script ? btemplates.pubKey.output.decode(script) : undefined signatures = chunks.slice(0, 1) break case scriptTypes.MULTISIG: if (script) { - var multisig = bscript.multisig.output.decode(script) + var multisig = btemplates.multisig.output.decode(script) pubKeys = multisig.pubKeys } @@ -72,24 +73,24 @@ function expandInput (scriptSig, witnessStack) { var chunks var scriptSigChunks = bscript.decompile(scriptSig) - var sigType = bscript.classifyInput(scriptSigChunks, true) + var sigType = btemplates.classifyInput(scriptSigChunks, true) if (sigType === scriptTypes.P2SH) { p2sh = true redeemScript = scriptSigChunks[scriptSigChunks.length - 1] - redeemScriptType = bscript.classifyOutput(redeemScript) - prevOutScript = bscript.scriptHash.output.encode(bcrypto.hash160(redeemScript)) + redeemScriptType = btemplates.classifyOutput(redeemScript) + prevOutScript = btemplates.scriptHash.output.encode(bcrypto.hash160(redeemScript)) prevOutType = scriptTypes.P2SH script = redeemScript } - var classifyWitness = bscript.classifyWitness(witnessStack, true) + var classifyWitness = btemplates.classifyWitness(witnessStack, true) if (classifyWitness === scriptTypes.P2WSH) { witnessScript = witnessStack[witnessStack.length - 1] - witnessScriptType = bscript.classifyOutput(witnessScript) + witnessScriptType = btemplates.classifyOutput(witnessScript) p2wsh = true witness = true if (scriptSig.length === 0) { - prevOutScript = bscript.witnessScriptHash.output.encode(bcrypto.sha256(witnessScript)) + prevOutScript = btemplates.witnessScriptHash.output.encode(bcrypto.sha256(witnessScript)) prevOutType = scriptTypes.P2WSH if (redeemScript !== undefined) { throw new Error('Redeem script given when unnecessary') @@ -99,13 +100,13 @@ function expandInput (scriptSig, witnessStack) { if (!redeemScript) { throw new Error('No redeemScript provided for P2WSH, but scriptSig non-empty') } - witnessProgram = bscript.witnessScriptHash.output.encode(bcrypto.sha256(witnessScript)) + witnessProgram = btemplates.witnessScriptHash.output.encode(bcrypto.sha256(witnessScript)) if (!redeemScript.equals(witnessProgram)) { throw new Error('Redeem script didn\'t match witnessScript') } } - if (!supportedType(bscript.classifyOutput(witnessScript))) { + if (!supportedType(btemplates.classifyOutput(witnessScript))) { throw new Error('unsupported witness script') } @@ -117,7 +118,7 @@ function expandInput (scriptSig, witnessStack) { var key = witnessStack[witnessStack.length - 1] var keyHash = bcrypto.hash160(key) if (scriptSig.length === 0) { - prevOutScript = bscript.witnessPubKeyHash.output.encode(keyHash) + prevOutScript = btemplates.witnessPubKeyHash.output.encode(keyHash) prevOutType = scriptTypes.P2WPKH if (typeof redeemScript !== 'undefined') { throw new Error('Redeem script given when unnecessary') @@ -126,7 +127,7 @@ function expandInput (scriptSig, witnessStack) { if (!redeemScript) { throw new Error('No redeemScript provided for P2WPKH, but scriptSig wasn\'t empty') } - witnessProgram = bscript.witnessPubKeyHash.output.encode(keyHash) + witnessProgram = btemplates.witnessPubKeyHash.output.encode(keyHash) if (!redeemScript.equals(witnessProgram)) { throw new Error('Redeem script did not have the right witness program') } @@ -143,7 +144,7 @@ function expandInput (scriptSig, witnessStack) { scriptType = redeemScriptType chunks = scriptSigChunks.slice(0, -1) } else { - prevOutType = scriptType = bscript.classifyInput(scriptSig) + prevOutType = scriptType = btemplates.classifyInput(scriptSig) chunks = scriptSigChunks } @@ -211,7 +212,7 @@ function expandOutput (script, scriptType, ourPubKey) { var scriptChunks = bscript.decompile(script) if (!scriptType) { - scriptType = bscript.classifyOutput(script) + scriptType = btemplates.classifyOutput(script) } var pubKeys = [] @@ -293,14 +294,14 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip witnessScriptHash = bcrypto.sha256(witnessScript) checkP2shInput(input, redeemScriptHash) - if (!redeemScript.equals(bscript.witnessScriptHash.output.encode(witnessScriptHash))) throw new Error('Witness script inconsistent with redeem script') + if (!redeemScript.equals(btemplates.witnessScriptHash.output.encode(witnessScriptHash))) throw new Error('Witness script inconsistent with redeem script') expanded = expandOutput(witnessScript, undefined, kpPubKey) if (!expanded.pubKeys) throw new Error('WitnessScript not supported "' + bscript.toASM(redeemScript) + '"') - prevOutType = bscript.types.P2SH - prevOutScript = bscript.scriptHash.output.encode(redeemScriptHash) + prevOutType = btemplates.types.P2SH + prevOutScript = btemplates.scriptHash.output.encode(redeemScriptHash) p2sh = witness = p2wsh = true - p2shType = bscript.types.P2WSH + p2shType = btemplates.types.P2WSH signType = witnessType = expanded.scriptType signScript = witnessScript } else if (redeemScript) { @@ -310,12 +311,12 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip expanded = expandOutput(redeemScript, undefined, kpPubKey) if (!expanded.pubKeys) throw new Error('RedeemScript not supported "' + bscript.toASM(redeemScript) + '"') - prevOutType = bscript.types.P2SH - prevOutScript = bscript.scriptHash.output.encode(redeemScriptHash) + prevOutType = btemplates.types.P2SH + prevOutScript = btemplates.scriptHash.output.encode(redeemScriptHash) p2sh = true signType = p2shType = expanded.scriptType signScript = redeemScript - witness = signType === bscript.types.P2WPKH + witness = signType === btemplates.types.P2WPKH } else if (witnessScript) { witnessScriptHash = bcrypto.sha256(witnessScript) checkP2WSHInput(input, witnessScriptHash) @@ -323,8 +324,8 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip expanded = expandOutput(witnessScript, undefined, kpPubKey) if (!expanded.pubKeys) throw new Error('WitnessScript not supported "' + bscript.toASM(redeemScript) + '"') - prevOutType = bscript.types.P2WSH - prevOutScript = bscript.witnessScriptHash.output.encode(witnessScriptHash) + prevOutType = btemplates.types.P2WSH + prevOutScript = btemplates.witnessScriptHash.output.encode(witnessScriptHash) witness = p2wsh = true signType = witnessType = expanded.scriptType signScript = witnessScript @@ -344,7 +345,7 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip signType = prevOutType signScript = prevOutScript } else { - prevOutScript = bscript.pubKeyHash.output.encode(bcrypto.hash160(kpPubKey)) + prevOutScript = btemplates.pubKeyHash.output.encode(bcrypto.hash160(kpPubKey)) expanded = expandOutput(prevOutScript, scriptTypes.P2PKH, kpPubKey) prevOutType = scriptTypes.P2PKH witness = false @@ -359,7 +360,7 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip } if (signType === scriptTypes.P2WPKH) { - signScript = bscript.pubKeyHash.output.encode(bscript.witnessPubKeyHash.output.decode(signScript)) + signScript = btemplates.pubKeyHash.output.encode(btemplates.witnessPubKeyHash.output.decode(signScript)) } if (p2sh) { @@ -383,9 +384,9 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip function buildStack (type, signatures, pubKeys, allowIncomplete) { if (type === scriptTypes.P2PKH) { - if (signatures.length === 1 && Buffer.isBuffer(signatures[0]) && pubKeys.length === 1) return bscript.pubKeyHash.input.encodeStack(signatures[0], pubKeys[0]) + if (signatures.length === 1 && Buffer.isBuffer(signatures[0]) && pubKeys.length === 1) return btemplates.pubKeyHash.input.encodeStack(signatures[0], pubKeys[0]) } else if (type === scriptTypes.P2PK) { - if (signatures.length === 1 && Buffer.isBuffer(signatures[0])) return bscript.pubKey.input.encodeStack(signatures[0]) + if (signatures.length === 1 && Buffer.isBuffer(signatures[0])) return btemplates.pubKey.input.encodeStack(signatures[0]) } else if (type === scriptTypes.MULTISIG) { if (signatures.length > 0) { signatures = signatures.map(function (signature) { @@ -396,7 +397,7 @@ function buildStack (type, signatures, pubKeys, allowIncomplete) { signatures = signatures.filter(function (x) { return x !== ops.OP_0 }) } - return bscript.multisig.input.encodeStack(signatures) + return btemplates.multisig.input.encodeStack(signatures) } } else { throw new Error('Not yet supported') @@ -416,7 +417,7 @@ function buildInput (input, allowIncomplete) { } var p2sh = false - if (scriptType === bscript.types.P2SH) { + if (scriptType === btemplates.types.P2SH) { // We can remove this error later when we have a guarantee prepareInput // rejects unsignable scripts - it MUST be signable at this point. if (!allowIncomplete && !supportedP2SHType(input.redeemScriptType)) { @@ -436,11 +437,11 @@ function buildInput (input, allowIncomplete) { switch (scriptType) { // P2WPKH is a special case of P2PKH - case bscript.types.P2WPKH: - witness = buildStack(bscript.types.P2PKH, input.signatures, input.pubKeys, allowIncomplete) + case btemplates.types.P2WPKH: + witness = buildStack(btemplates.types.P2PKH, input.signatures, input.pubKeys, allowIncomplete) break - case bscript.types.P2WSH: + case btemplates.types.P2WSH: // We can remove this check later if (!allowIncomplete && !supportedType(input.witnessScriptType)) { throw new Error('Impossible to sign this type') @@ -593,7 +594,7 @@ TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, options) } input.prevOutScript = options.prevOutScript - input.prevOutType = prevOutType || bscript.classifyOutput(options.prevOutScript) + input.prevOutType = prevOutType || btemplates.classifyOutput(options.prevOutScript) } var vin = this.tx.addInput(txHash, vout, options.sequence, options.scriptSig) @@ -637,7 +638,7 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) { // skip if no result if (!allowIncomplete) { - if (!supportedType(result.type) && result.type !== bscript.types.P2WPKH) { + if (!supportedType(result.type) && result.type !== btemplates.types.P2WPKH) { throw new Error(result.type + ' not supported') } } diff --git a/test/transaction_builder.js b/test/transaction_builder.js index 2ad4c93..b98273a 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -3,6 +3,7 @@ var assert = require('assert') var baddress = require('../src/address') var bscript = require('../src/script') +var btemplates = require('../src/templates') var ops = require('bitcoin-ops') var BigInteger = require('bigi') @@ -434,7 +435,7 @@ describe('TransactionBuilder', function () { var signatures = bscript.decompile(scriptSig).slice(1, -1).filter(function (x) { return x !== ops.OP_0 }) // rebuild/replace the scriptSig without them - var replacement = bscript.scriptHash.input.encode(bscript.multisig.input.encode(signatures), redeemScript) + var replacement = btemplates.scriptHash.input.encode(btemplates.multisig.input.encode(signatures), redeemScript) assert.strictEqual(bscript.toASM(replacement), sign.scriptSigFiltered) tx.ins[i].script = replacement From c729d322c22d14f9be852168cded7af89824c695 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 27 Sep 2017 07:08:31 +1000 Subject: [PATCH 2/3] tests: add failing staged transaction building example #901 --- test/fixtures/transaction_builder.json | 35 ++++++++++++++++++++++++++ test/transaction_builder.js | 7 ++++++ 2 files changed, 42 insertions(+) diff --git a/test/fixtures/transaction_builder.json b/test/fixtures/transaction_builder.json index 9fcb2cb..1889363 100644 --- a/test/fixtures/transaction_builder.json +++ b/test/fixtures/transaction_builder.json @@ -1386,6 +1386,41 @@ "value": 99000 } ] + }, + { + "description": "P2WSH(multisig 2-of-3) -> P2PKH", + "network": "testnet", + "txHex": "01000000000101ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01000000232200201b48bf145648b9492ecd6d76754ea3def4b90e22e4ef7aee9ca291b2de455701ffffffff01f07e0e00000000001976a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac0400473044022036c9ecb03cb04c09be1f52766725dcfe9a815973bd2f34ce19a345f2d925a45502207b90737852d2508db104ad17612de473687e67928c045555a1ed8d495c0570d901483045022100aec0e58e4e597b35ca5a727702a0da3d4f2ef4759914da7fc80aecb3c479a6d902201ec27ea8dcca4b73ee81e4b627f52f9e627c3497f61e4beeb98f86e02979640a0169522103c411cf39aca4395c81c35921dc832a0d1585d652ab1b52ccc619ff9fbbc5787721020636d944458a4663b75a912c37dc1cd59b11f9a00106783a65ba230d929b96b02102d1448cbf19528a1a27e5958ba73d930b5b3facdbe5c30c7094951a287fcc914953ae00000000", + "stages": [ + "01000000000101ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff01000000232200201b48bf145648b9492ecd6d76754ea3def4b90e22e4ef7aee9ca291b2de455701ffffffff01f07e0e00000000001976a914aa4d7985c57e011a8b3dd8e0e5a73aaef41629c588ac0500473044022036c9ecb03cb04c09be1f52766725dcfe9a815973bd2f34ce19a345f2d925a45502207b90737852d2508db104ad17612de473687e67928c045555a1ed8d495c0570d901000069522103c411cf39aca4395c81c35921dc832a0d1585d652ab1b52ccc619ff9fbbc5787721020636d944458a4663b75a912c37dc1cd59b11f9a00106783a65ba230d929b96b02102d1448cbf19528a1a27e5958ba73d930b5b3facdbe5c30c7094951a287fcc914953ae00000000" + ], + "inputs": [ + { + "txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "vout": 1, + "signs": [ + { + "keyPair": "cUxccFVBdJRq6HnyxiFMd8Z15GLThXaNLcnPBgoXLEv9iX6wuV2b", + "witnessScript": "OP_2 03c411cf39aca4395c81c35921dc832a0d1585d652ab1b52ccc619ff9fbbc57877 020636d944458a4663b75a912c37dc1cd59b11f9a00106783a65ba230d929b96b0 02d1448cbf19528a1a27e5958ba73d930b5b3facdbe5c30c7094951a287fcc9149 OP_3 OP_CHECKMULTISIG", + "redeemScript": "OP_0 1b48bf145648b9492ecd6d76754ea3def4b90e22e4ef7aee9ca291b2de455701", + "value": 1000000, + "stage": true + }, + { + "keyPair": "cVSNe9ZdZRsRvEBL8YRR7YiZmH4cLsf5FthgERWkZezJVrGseaXy", + "witnessScript": "OP_2 03c411cf39aca4395c81c35921dc832a0d1585d652ab1b52ccc619ff9fbbc57877 020636d944458a4663b75a912c37dc1cd59b11f9a00106783a65ba230d929b96b0 02d1448cbf19528a1a27e5958ba73d930b5b3facdbe5c30c7094951a287fcc9149 OP_3 OP_CHECKMULTISIG", + "redeemScript": "OP_0 1b48bf145648b9492ecd6d76754ea3def4b90e22e4ef7aee9ca291b2de455701", + "value": 1000000 + } + ] + } + ], + "outputs": [ + { + "script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG", + "value": 950000 + } + ] } ], "fromTransaction": [ diff --git a/test/transaction_builder.js b/test/transaction_builder.js index b98273a..82dda19 100644 --- a/test/transaction_builder.js +++ b/test/transaction_builder.js @@ -51,6 +51,7 @@ function construct (f, dontSign) { if (dontSign) return txb + var stages = f.stages && f.stages.concat() f.inputs.forEach(function (input, index) { if (!input.signs) return input.signs.forEach(function (sign) { @@ -68,6 +69,12 @@ function construct (f, dontSign) { witnessScript = bscript.fromASM(sign.witnessScript) } txb.sign(index, keyPair, redeemScript, sign.hashType, value, witnessScript) + + if (sign.stage) { + var tx = txb.buildIncomplete() + assert.strictEqual(tx.toHex(), stages.shift()) + txb = TransactionBuilder.fromTransaction(tx, network) + } }) }) From 41378f96486e87f0f1642f85545fa05c8f8aac2b Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Wed, 27 Sep 2017 07:22:03 +1000 Subject: [PATCH 3/3] txbuilder: apply input.value before prepareInput --- src/transaction_builder.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/transaction_builder.js b/src/transaction_builder.js index 90e5e65..421e4f4 100644 --- a/src/transaction_builder.js +++ b/src/transaction_builder.js @@ -298,6 +298,7 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip expanded = expandOutput(witnessScript, undefined, kpPubKey) if (!expanded.pubKeys) throw new Error('WitnessScript not supported "' + bscript.toASM(redeemScript) + '"') + prevOutType = btemplates.types.P2SH prevOutScript = btemplates.scriptHash.output.encode(redeemScriptHash) p2sh = witness = p2wsh = true @@ -347,18 +348,13 @@ function prepareInput (input, kpPubKey, redeemScript, witnessValue, witnessScrip } else { prevOutScript = btemplates.pubKeyHash.output.encode(bcrypto.hash160(kpPubKey)) expanded = expandOutput(prevOutScript, scriptTypes.P2PKH, kpPubKey) + prevOutType = scriptTypes.P2PKH witness = false signType = prevOutType signScript = prevOutScript } - if (witnessValue !== undefined || witness) { - typeforce(types.Satoshi, witnessValue) - if (input.value !== undefined && input.value !== witnessValue) throw new Error('Input didn\'t match witnessValue') - input.value = witnessValue - } - if (signType === scriptTypes.P2WPKH) { signScript = btemplates.pubKeyHash.output.encode(btemplates.witnessPubKeyHash.output.decode(signScript)) } @@ -686,7 +682,13 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy var kpPubKey = keyPair.getPublicKeyBuffer() if (!canSign(input)) { - prepareInput(input, kpPubKey, redeemScript, witnessValue, witnessScript) + if (witnessValue !== undefined) { + if (input.value !== undefined && input.value !== witnessValue) throw new Error('Input didn\'t match witnessValue') + typeforce(types.Satoshi, witnessValue) + input.value = witnessValue + } + + if (!canSign(input)) prepareInput(input, kpPubKey, redeemScript, witnessValue, witnessScript) if (!canSign(input)) throw Error(input.prevOutType + ' not supported') }