From 76323a07d04925e2448a22836cad14160e367a14 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 21:19:09 +1000 Subject: [PATCH 01/15] Transaction: restrict TxIn/TxOut constructor params To keep this change minimal, both TxIn/TxOut still use the parameter object for initialization. TxOut accepts only the types it uses internally, and not hex or byte arrays for scripts. The clone is unnecessary as a TransactionOut is never mutated after its creation. This resulted in TransactionOut.scriptPubKey no longer being needed, and was removed. To access the scriptPubKey as a byte buffer, a user can simply use: TransactionOut.script.toBuffer() Unfortunately, this leaves TransactionOut in a sorry state of test. Something that needs to be fixed. --- src/transaction.js | 39 +++++++++------------------------------ test/transaction.js | 12 ------------ 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/src/transaction.js b/src/transaction.js index 72f09fc..64bb41a 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -458,14 +458,8 @@ var TransactionIn = function (data) { this.outpoint = { hash: data.hash, index: data.index } } - if (data.scriptSig) { - this.script = Script.fromScriptSig(data.scriptSig) - } else if (data.script) { - this.script = data.script - } else { - this.script = new Script(data.script) - } - + assert(data.script, 'Invalid TxIn parameters') + this.script = data.script this.sequence = data.sequence || this.defaultSequence } @@ -480,38 +474,23 @@ TransactionIn.prototype.clone = function () { }) } -// FIXME: Support for alternate networks -var TransactionOut = function (data) { - this.script = - data.script instanceof Script ? data.script.clone() - : Array.isArray(data.script) ? new Script(data.script) - : typeof data.script == "string" ? new Script(convert.hexToBytes(data.script)) - : data.scriptPubKey ? Script.fromScriptSig(data.scriptPubKey) - : data.address ? Script.createOutputScript(data.address) - : new Script() +function TransactionOut(data) { + this.script = data.script + this.value = data.value + this.address = data.address var network = data.network || Network.bitcoin if (this.script.buffer.length > 0) { this.address = this.script.getToAddress(network) } - - this.value = - Array.isArray(data.value) ? convert.bytesToNum(data.value) - : "string" == typeof data.value ? parseInt(data.value) - : data.value instanceof BigInteger ? parseInt(data.value.toString()) - : data.value } TransactionOut.prototype.clone = function() { - var newTxout = new TransactionOut({ + return new TransactionOut({ script: this.script.clone(), - value: this.value + value: this.value, + address: this.address }) - return newTxout -} - -TransactionOut.prototype.scriptPubKey = function() { - return convert.bytesToHex(this.script.buffer) } module.exports = { diff --git a/test/transaction.js b/test/transaction.js index e672411..234d925 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -279,18 +279,6 @@ describe('Transaction', function() { }) describe('TransactionOut', function() { - describe('scriptPubKey', function() { - it('returns hex string', function() { - var address = Address.fromBase58Check("1AZpKpcfCzKDUeTFBQUL4MokQai3m3HMXv") - - var txOut = new TransactionOut({ - value: 50000, - script: Script.createOutputScript(address) - }) - - assert.equal(txOut.scriptPubKey(), "76a91468edf28474ee22f68dfe7e56e76c017c1701b84f88ac") - }) - }) }) }) From 74e53b49a59f8ddd0c7d6c7673c8769f5dd70ed9 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 13:05:16 +1000 Subject: [PATCH 02/15] Script/Transaction: remove untested functions These functions are not under test, and are unnecessary bloat due to a confusing API. Script.from*(asmStr) were two functions that attempted to parse ASM codes and produce a script from this. While useful, an parser can be introduced later under a single function and under test... removed. Although Script.extractPublicKeys implementation is likely to be correct, it is not absolute in that what it returns is even strictly a set of public keys. It is a useful function, but can be done in a better way later, probably checking against the Script templates instead. Transaction.signWithKeys has some inherent undocumented behaviour, and it is not clear when you would use it over just Transaction.addOutput and Transaction.sign individually. Nor does it mimic anything in the bitcoind API... removed. --- src/address.js | 1 - src/script.js | 36 ------------------------------------ src/transaction.js | 34 ---------------------------------- 3 files changed, 71 deletions(-) diff --git a/src/address.js b/src/address.js index d1d7b82..5457e52 100644 --- a/src/address.js +++ b/src/address.js @@ -16,7 +16,6 @@ Address.fromBase58Check = function(string) { return new Address(decode.payload, decode.version) } -Address.prototype.fromString = Address.prototype.fromBase58Check // Export functions Address.prototype.toBase58Check = function () { diff --git a/src/script.js b/src/script.js index 8f3cd2c..f550878 100644 --- a/src/script.js +++ b/src/script.js @@ -23,32 +23,6 @@ Script.fromHex = function(hex) { return Script.fromBuffer(new Buffer(hex, 'hex')) } -Script.fromPubKey = function(str) { - var script = new Script() - var s = str.split(' ') - for (var i in s) { - if (Opcode.map.hasOwnProperty(s[i])) { - script.writeOp(Opcode.map[s[i]]) - } else { - script.writeBytes(convert.hexToBytes(s[i])) - } - } - return script -} - -Script.fromScriptSig = function(str) { - var script = new Script() - var s = str.split(' ') - for (var i in s) { - if (Opcode.map.hasOwnProperty(s[i])) { - script.writeOp(Opcode.map[s[i]]) - } else { - script.writeBytes(convert.hexToBytes(s[i])) - } - } - return script -} - /** * Update the parsed script representation. * @@ -399,16 +373,6 @@ Script.createOutputScript = function(address, network) { return script } -/** - * Extract pubkeys from a multisig script - */ - -Script.prototype.extractPubkeys = function() { - return this.chunks.filter(function(chunk) { - return(chunk[0] == 4 && chunk.length == 65 || chunk[0] < 4 && chunk.length == 33) - }) -} - // m [pubKeys ...] n OP_CHECKMULTISIG Script.createMultisigOutputScript = function(m, pubKeys) { var script = new Script() diff --git a/src/transaction.js b/src/transaction.js index 64bb41a..0b346a4 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -380,40 +380,6 @@ Transaction.prototype.sign = function(index, key, type, network) { this.setScriptSig(index, scriptSig) } -// Takes outputs of the form [{ output: 'txhash:index', address: 'address' },...] -Transaction.prototype.signWithKeys = function(keys, outputs, type) { - type = type || SIGHASH_ALL - - var addrdata = keys.map(function(key) { - assert(key instanceof ECKey) - - return { - key: key, - address: key.getAddress().toString() - } - }) - - var hmap = {} - outputs.forEach(function(o) { - hmap[o.output] = o - }) - - for (var i = 0; i < this.ins.length; i++) { - var outpoint = this.ins[i].outpoint.hash + ':' + this.ins[i].outpoint.index - var histItem = hmap[outpoint] - - if (!histItem) continue; - - var thisInputAddrdata = addrdata.filter(function(a) { - return a.address == histItem.address - }) - - if (thisInputAddrdata.length === 0) continue; - - this.sign(i,thisInputAddrdata[0].key) - } -} - Transaction.prototype.signScriptSig = function(index, script, key, type) { type = type || SIGHASH_ALL From 7ad695e427c4ae78b1d6a151c5d7fa19ffe67cd9 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 13:04:07 +1000 Subject: [PATCH 03/15] Script: fix assertion message grammar --- src/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/script.js b/src/script.js index f550878..f672960 100644 --- a/src/script.js +++ b/src/script.js @@ -321,7 +321,7 @@ Script.prototype.writeOp = function(opcode) { Script.prototype.writeBytes = function(data) { // FIXME: Script module doesn't support buffers yet if (Buffer.isBuffer(data)) data = Array.prototype.slice.call(data); - assert(Array.isArray(data), "Expect a byte array. Got" + data) + assert(Array.isArray(data), "Expected a byte array. Got " + data) if (data.length < Opcode.map.OP_PUSHDATA1) { this.buffer.push(data.length) From 929b8d2b620a14844ba8299d45b410a9c1180635 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 13:07:19 +1000 Subject: [PATCH 04/15] Script: more consistent input handling --- src/script.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/script.js b/src/script.js index f672960..6fa6269 100644 --- a/src/script.js +++ b/src/script.js @@ -6,10 +6,10 @@ var Network = require('./network') var Opcode = require('./opcode') function Script(data) { - this.buffer = data || [] - if(!Array.isArray(this.buffer)) { - throw new Error('expect Script to be initialized with Array, but got ' + data) - } + data = data || [] + assert(Array.isArray(data), 'Expected Array, got ' + data) + + this.buffer = data this.parse() } From 57b8afbdabc1d2a1872e07f20a987e4358371590 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 13:23:22 +1000 Subject: [PATCH 05/15] Network: rename Network to Networks This change removes the most common ambiguities. As the network module is not representative of a class, the lower case has been used. --- src/eckey.js | 6 +++--- src/hdwallet.js | 12 ++++++------ src/index.js | 2 +- src/{network.js => networks.js} | 0 src/script.js | 8 ++++---- src/transaction.js | 8 ++++---- src/wallet.js | 10 +++++----- test/integration/p2sh.js | 6 +++--- test/message.js | 4 ++-- test/script.js | 4 ++-- test/transaction.js | 6 +++--- 11 files changed, 33 insertions(+), 33 deletions(-) rename src/{network.js => networks.js} (100%) diff --git a/src/eckey.js b/src/eckey.js index 87d2934..fa238a9 100644 --- a/src/eckey.js +++ b/src/eckey.js @@ -1,7 +1,7 @@ var assert = require('assert') var base58check = require('./base58check') var ecdsa = require('./ecdsa') -var network = require('./network') +var networks = require('./networks') var secureRandom = require('secure-random') var Address = require('./address') @@ -72,7 +72,7 @@ ECKey.prototype.toHex = function() { } ECKey.prototype.toWIF = function(version) { - version = version || network.bitcoin.wif + version = version || networks.bitcoin.wif var buffer = this.toBuffer() if (this.pub.compressed) { @@ -115,7 +115,7 @@ ECPubKey.prototype.verify = function(hash, sig) { } ECPubKey.prototype.getAddress = function(version) { - version = version || network.bitcoin.pubKeyHash + version = version || networks.bitcoin.pubKeyHash return new Address(crypto.hash160(this.toBuffer()), version) } diff --git a/src/hdwallet.js b/src/hdwallet.js index 146b82d..966185d 100644 --- a/src/hdwallet.js +++ b/src/hdwallet.js @@ -8,7 +8,7 @@ var CJS = require('crypto-js') var crypto = require('./crypto') var ECKey = require('./eckey').ECKey var ECPubKey = require('./eckey').ECPubKey -var Network = require('./network') +var networks = require('./networks') var sec = require('./sec') var ecparams = sec("secp256k1") @@ -27,7 +27,7 @@ function HDWallet(seed, networkString) { this.chaincode = I.slice(32) this.network = networkString || 'bitcoin' - if(!Network.hasOwnProperty(this.network)) { + if(!networks.hasOwnProperty(this.network)) { throw new Error("Unknown network: " + this.network) } @@ -70,8 +70,8 @@ HDWallet.fromBuffer = function(input) { var version = input.readUInt32BE(0) var type - for(var name in Network) { - var network = Network[name] + for(var name in networks) { + var network = networks[name] for(var t in network.bip32) { if (version != network.bip32[t]) continue @@ -128,7 +128,7 @@ HDWallet.prototype.getAddress = function() { HDWallet.prototype.toBuffer = function(priv) { // Version - var version = Network[this.network].bip32[priv ? 'priv' : 'pub'] + var version = networks[this.network].bip32[priv ? 'priv' : 'pub'] var buffer = new Buffer(HDWallet.LENGTH) // 4 bytes: version bytes @@ -245,7 +245,7 @@ HDWallet.prototype.derivePrivate = function(index) { } HDWallet.prototype.getKeyVersion = function() { - return Network[this.network].pubKeyHash + return networks[this.network].pubKeyHash } HDWallet.prototype.toString = HDWallet.prototype.toBase58 diff --git a/src/index.js b/src/index.js index c236a02..484342d 100644 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,6 @@ module.exports = { Transaction: T.Transaction, TransactionIn: T.TransactionIn, TransactionOut: T.TransactionOut, - network: require('./network'), + networks: require('./networks'), Wallet: require('./wallet') } diff --git a/src/network.js b/src/networks.js similarity index 100% rename from src/network.js rename to src/networks.js diff --git a/src/script.js b/src/script.js index 6fa6269..c5bec6b 100644 --- a/src/script.js +++ b/src/script.js @@ -2,7 +2,7 @@ var assert = require('assert') var Address = require('./address') var crypto = require('./crypto') var convert = require('./convert') -var Network = require('./network') +var networks = require('./networks') var Opcode = require('./opcode') function Script(data) { @@ -193,7 +193,7 @@ Script.prototype.toScriptHash = function() { } Script.prototype.getToAddress = function(network) { - network = network || Network.bitcoin + network = network || networks.bitcoin if(isPubkeyhash.call(this)) { return new Address(new Buffer(this.chunks[2]), network.pubKeyHash) @@ -205,7 +205,7 @@ Script.prototype.getToAddress = function(network) { } Script.prototype.getFromAddress = function(version) { - version = version || Network.bitcoin.pubKeyHash + version = version || networks.bitcoin.pubKeyHash return new Address(this.simpleInHash(), version) } @@ -348,7 +348,7 @@ Script.prototype.writeBytes = function(data) { */ Script.createOutputScript = function(address, network) { assert(address instanceof Address) - network = network || Network.bitcoin + network = network || networks.bitcoin var script = new Script() diff --git a/src/transaction.js b/src/transaction.js index 0b346a4..f6660bc 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -9,7 +9,7 @@ var convert = require('./convert') var crypto = require('./crypto') var ECKey = require('./eckey').ECKey var ecdsa = require('./ecdsa') -var Network = require('./network') +var networks = require('./networks') var Transaction = function (doc) { if (!(this instanceof Transaction)) { return new Transaction(doc) } @@ -112,7 +112,7 @@ Transaction.prototype.addOutput = function (address, value, network) { address = Address.fromBase58Check(address) } - network = network || Network.bitcoin + network = network || networks.bitcoin this.outs.push(new TransactionOut({ value: value, @@ -368,7 +368,7 @@ Transaction.deserialize = function(buffer) { */ Transaction.prototype.sign = function(index, key, type, network) { assert(key instanceof ECKey) - network = network || Network.bitcoin + network = network || networks.bitcoin var address = key.pub.getAddress(network.pubKeyHash) @@ -445,7 +445,7 @@ function TransactionOut(data) { this.value = data.value this.address = data.address - var network = data.network || Network.bitcoin + var network = data.network || networks.bitcoin if (this.script.buffer.length > 0) { this.address = this.script.getToAddress(network) } diff --git a/src/wallet.js b/src/wallet.js index e843dba..c6b7134 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -2,7 +2,7 @@ var convert = require('./convert') var Transaction = require('./transaction').Transaction var HDNode = require('./hdwallet.js') var rng = require('secure-random') -var Network = require('./network') +var networks = require('./networks') function Wallet(seed, options) { if (!(this instanceof Wallet)) { return new Wallet(seed, options); } @@ -174,7 +174,7 @@ function Wallet(seed, options) { checkDust(value) var tx = new Transaction() - tx.addOutput(to, value, Network[network]) + tx.addOutput(to, value, networks[network]) var utxo = getCandidateOutputs(value) var totalInValue = 0 @@ -190,7 +190,7 @@ function Wallet(seed, options) { var change = totalInValue - value - fee if(change > 0 && !isDust(change)) { - tx.addOutput(changeAddress || getChangeAddress(), change, Network[network]) + tx.addOutput(changeAddress || getChangeAddress(), change, networks[network]) } break } @@ -246,7 +246,7 @@ function Wallet(seed, options) { function estimateFeePadChangeOutput(tx){ var tmpTx = tx.clone() - tmpTx.addOutput(getChangeAddress(), 0, Network[network]) + tmpTx.addOutput(getChangeAddress(), 0, networks[network]) return tmpTx.estimateFee() } @@ -266,7 +266,7 @@ function Wallet(seed, options) { tx.ins.forEach(function(inp,i) { var output = me.outputs[inp.outpoint.hash + ':' + inp.outpoint.index] if (output) { - tx.sign(i, me.getPrivateKeyForAddress(output.address), false, Network[network]) + tx.sign(i, me.getPrivateKeyForAddress(output.address), false, networks[network]) } }) return tx diff --git a/test/integration/p2sh.js b/test/integration/p2sh.js index b52f9a6..55978bb 100644 --- a/test/integration/p2sh.js +++ b/test/integration/p2sh.js @@ -5,7 +5,7 @@ var ECKey = require('../../src/eckey').ECKey; var T = require('../../src/transaction'); var Transaction = T.Transaction; var Script = require('../../src/script'); -var network = require('../../src/network'); +var networks = require('../../src/networks'); var crypto = require('../../src/crypto'); var helloblock = require('helloblock-js')({ @@ -31,7 +31,7 @@ describe('p2sh', function() { }) var redeemScript = Script.createMultisigOutputScript(2, pubKeyBuffers) var hash160 = crypto.hash160(redeemScript.buffer) - var multisigAddress = new Address(hash160, network.testnet.scriptHash) + var multisigAddress = new Address(hash160, networks.testnet.scriptHash) // Check what our target address's starting value is var targetAddress = 'mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r'; @@ -50,7 +50,7 @@ describe('p2sh', function() { var tx = new Transaction() var unspent = resource[0]; tx.addInput(unspent.txHash, unspent.index) - tx.addOutput(targetAddress, 100000, network.testnet) + tx.addOutput(targetAddress, 100000, networks.testnet) var signatures = privKeys.map(function(privKey) { return tx.signScriptSig(0, redeemScript, privKey) diff --git a/test/message.js b/test/message.js index 6589250..12831da 100644 --- a/test/message.js +++ b/test/message.js @@ -1,7 +1,7 @@ var assert = require('assert') var ECKey = require('../src/eckey').ECKey var Message = require('../').Message -var network = require('../').network +var networks = require('../').networks var fixtures = require('./fixtures/message') @@ -86,7 +86,7 @@ describe('Message', function() { var key = ECKey.makeRandom() var sig = Message.sign(key, msg) - var addr = key.pub.getAddress(network.testnet.pubKeyHash) + var addr = key.pub.getAddress(networks.testnet.pubKeyHash) assert(Message.verify(addr, sig, msg)) }) }) diff --git a/test/script.js b/test/script.js index 3d15610..ea84681 100644 --- a/test/script.js +++ b/test/script.js @@ -1,6 +1,6 @@ var assert = require('assert') var crypto = require('..').crypto -var network = require('..').network +var networks = require('..').networks var Address = require('../src/address.js') var Script = require('../src/script.js') @@ -116,7 +116,7 @@ describe('Script', function() { var redeemScript = Script.createMultisigOutputScript(2, pubKeys) var hash160 = crypto.hash160(redeemScript.buffer) - var multisigAddress = new Address(hash160, network.bitcoin.scriptHash) + var multisigAddress = new Address(hash160, networks.bitcoin.scriptHash) assert.equal(multisigAddress.toString(), '32vYjxBb7pHJJyXgNk8UoK3BdRDxBzny2v') }) diff --git a/test/transaction.js b/test/transaction.js index 234d925..55f74e7 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -2,11 +2,11 @@ var assert = require('assert') var Address = require('../src/address') var ECKey = require('../src/eckey').ECKey +var networks = require('..').networks var T = require('../src/transaction') var Transaction = T.Transaction var TransactionOut = T.TransactionOut var Script = require('../src/script') -var network = require('..').network var fixtureTxes = require('./fixtures/mainnet_tx') var fixtureTx1Hex = fixtureTxes.prevTx @@ -176,7 +176,7 @@ describe('Transaction', function() { it('supports alternative networks', function(){ var addr = 'mkHJaNR7uuwRG1JrmTZsV4MszaTKjCBvCR' - tx.addOutput(addr, 40000, network.testnet) + tx.addOutput(addr, 40000, networks.testnet) verifyTransactionOut() assert.equal(tx.outs[0].address.toString(), addr) @@ -250,7 +250,7 @@ describe('Transaction', function() { it('works for multi-sig redeem script', function() { var tx = new Transaction() tx.addInput('d6f72aab8ff86ff6289842a0424319bf2ddba85dc7c52757912297f948286389', 0) - tx.addOutput('mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', 1, network.testnet) + tx.addOutput('mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', 1, networks.testnet) var privKeys = [ '5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf', From d0f684844ce83e6576e573f1900a3c3af94284df Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 14:44:45 +1000 Subject: [PATCH 06/15] Script: add toHex, fromHex Script.fromHex previously existed, but was not under any kind of test. This commit adds tests (despite being a little circular in nature) to check that the output is as expected. --- src/script.js | 10 ++++++++++ test/script.js | 24 ++++++++++++++++++++---- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/script.js b/src/script.js index c5bec6b..a58ab73 100644 --- a/src/script.js +++ b/src/script.js @@ -13,6 +13,7 @@ function Script(data) { this.parse() } +// Import operations Script.fromBuffer = function(buffer) { assert(Buffer.isBuffer(buffer)) // FIXME: transitionary @@ -23,6 +24,15 @@ Script.fromHex = function(hex) { return Script.fromBuffer(new Buffer(hex, 'hex')) } +// Export operations +Script.prototype.toBuffer = function() { + return new Buffer(this.buffer) +} + +Script.prototype.toHex = function() { + return this.toBuffer().toString('hex') +} + /** * Update the parsed script representation. * diff --git a/test/script.js b/test/script.js index ea84681..90f6ef3 100644 --- a/test/script.js +++ b/test/script.js @@ -9,11 +9,11 @@ function b2h(b) { return new Buffer(b).toString('hex') } function h2b(h) { return new Buffer(h, 'hex') } describe('Script', function() { - var p2shScriptPubKey, pubkeyScriptPubkey, addressScriptSig + var p2shScriptPubKey, pubKeyScriptPubKey, addressScriptSig beforeEach(function(){ p2shScriptPubKey = "a914e8c300c87986efa84c37c0519929019ef86eb5b487" - pubkeyScriptPubKey = "76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac" + pubKeyScriptPubKey = "76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac" addressScriptSig = "48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8" // txid: 09dd94f2c85262173da87a745a459007bb1eed6eeb6bfa238a0cd91a16cf7790 @@ -43,6 +43,22 @@ describe('Script', function() { }) }) + describe('fromHex/toHex', function() { + it('matches the test data', function() { + [ + p2shScriptPubKey, + pubKeyScriptPubKey, + addressScriptSig, + validMultisigScript, + opreturnScript, + nonStandardScript, + invalidMultisigScript + ].forEach(function(hex) { + assert.equal(Script.fromHex(hex).toHex(), hex) + }) + }) + }) + describe('getOutType', function() { it('supports p2sh', function() { var script = Script.fromHex(p2shScriptPubKey) @@ -50,7 +66,7 @@ describe('Script', function() { }) it('supports pubkeyhash', function() { - var script = Script.fromHex(pubkeyScriptPubKey) + var script = Script.fromHex(pubKeyScriptPubKey) assert.equal(script.getOutType(), 'pubkeyhash') }) @@ -89,7 +105,7 @@ describe('Script', function() { }) it('works for pubkey type output', function() { - var script = Script.fromHex(pubkeyScriptPubKey) + var script = Script.fromHex(pubKeyScriptPubKey) assert.equal(script.getToAddress().toString(), '19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') }) }) From 0822def7e089e6741f83493485c76a285aa6f704 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 16:20:54 +1000 Subject: [PATCH 07/15] Script: add standard Script.create*ScriptPubKey Extracts the two Script types out of Script.createOutputScript, and puts them both under test. Also renames Script.createMultiSigOutputScript to adhere to the same convention. --- src/script.js | 38 ++++++++++++++++++++++++-------------- src/transaction.js | 4 ++-- test/script.js | 22 ++++++++++++++++++++-- test/transaction.js | 2 +- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/script.js b/src/script.js index a58ab73..97aa070 100644 --- a/src/script.js +++ b/src/script.js @@ -356,35 +356,45 @@ Script.prototype.writeBytes = function(data) { /** * Create an output for an address */ -Script.createOutputScript = function(address, network) { +Script.createScriptPubKey = function(address, network) { assert(address instanceof Address) network = network || networks.bitcoin - var script = new Script() - - // Standard pay-to-script-hash - if (address.version === network.scriptHash) { - script.writeOp(Opcode.map.OP_HASH160) - script.writeBytes(address.hash) - script.writeOp(Opcode.map.OP_EQUAL) - - return script + if (address.version === network.pubKeyHash) { + return Script.createPubKeyHashScriptPubKey(address.hash) } - assert.strictEqual(address.version, network.pubKeyHash, 'Unknown address type') + assert.strictEqual(address.version, network.scriptHash, 'Unknown address type') + + return Script.createP2SHScriptPubKey(address.hash) +} + +// OP_DUP OP_HASH160 {pubKeyHash} OP_EQUALVERIFY OP_CHECKSIG +Script.createPubKeyHashScriptPubKey = function(hash) { + var script = new Script() - // Standard pay-to-pubkey-hash script.writeOp(Opcode.map.OP_DUP) script.writeOp(Opcode.map.OP_HASH160) - script.writeBytes(address.hash) + script.writeBytes(hash) script.writeOp(Opcode.map.OP_EQUALVERIFY) script.writeOp(Opcode.map.OP_CHECKSIG) return script } +// OP_HASH160 {scriptHash} OP_EQUAL +Script.createP2SHScriptPubKey = function(hash) { + var script = new Script() + + script.writeOp(Opcode.map.OP_HASH160) + script.writeBytes(hash) + script.writeOp(Opcode.map.OP_EQUAL) + + return script +} + // m [pubKeys ...] n OP_CHECKMULTISIG -Script.createMultisigOutputScript = function(m, pubKeys) { +Script.createMultisigScriptPubKey = function(m, pubKeys) { var script = new Script() pubKeys = pubKeys.sort() diff --git a/src/transaction.js b/src/transaction.js index f6660bc..49b1a4a 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -116,7 +116,7 @@ Transaction.prototype.addOutput = function (address, value, network) { this.outs.push(new TransactionOut({ value: value, - script: Script.createOutputScript(address, network), + script: Script.createScriptPubKey(address, network), network: network })) } @@ -373,7 +373,7 @@ Transaction.prototype.sign = function(index, key, type, network) { var address = key.pub.getAddress(network.pubKeyHash) // FIXME: Assumed prior TX was pay-to-pubkey-hash - var script = Script.createOutputScript(address, network) + var script = Script.createScriptPubKey(address, network) var signature = this.signScriptSig(index, script, key, type) var scriptSig = Script.createPubKeyHashScriptSig(signature, key.pub) diff --git a/test/script.js b/test/script.js index 90f6ef3..303d181 100644 --- a/test/script.js +++ b/test/script.js @@ -98,6 +98,24 @@ describe('Script', function() { }) }) + describe('pay-to-pubKeyHash', function() { + it('matches the test data', function() { + var address = Address.fromBase58Check('19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') + var script = Script.createPubKeyHashScriptPubKey(address.hash) + + assert.equal(script.toHex(), pubKeyScriptPubKey) + }) + }) + + describe('pay-to-scriptHash', function() { + it('matches the test data', function() { + var hash = new Buffer('e8c300c87986efa84c37c0519929019ef86eb5b4', 'hex') + var script = Script.createP2SHScriptPubKey(hash) + + assert.equal(script.toHex(), p2shScriptPubKey) + }) + }) + describe('getToAddress', function() { it('works for p2sh type output', function() { var script = Script.fromHex(p2shScriptPubKey) @@ -129,7 +147,7 @@ describe('Script', function() { }) it('should create valid redeemScript', function() { - var redeemScript = Script.createMultisigOutputScript(2, pubKeys) + var redeemScript = Script.createMultisigScriptPubKey(2, pubKeys) var hash160 = crypto.hash160(redeemScript.buffer) var multisigAddress = new Address(hash160, networks.bitcoin.scriptHash) @@ -150,7 +168,7 @@ describe('Script', function() { var expected = '0047304402207515cf147d201f411092e6be5a64a6006f9308fad7b2a8fdaab22cd86ce764c202200974b8aca7bf51dbf54150d3884e1ae04f675637b926ec33bf75939446f6ca2801483045022100ef253c1faa39e65115872519e5f0a33bbecf430c0f35cf562beabbad4da24d8d02201742be8ee49812a73adea3007c9641ce6725c32cd44ddb8e3a3af460015d14050147522102359c6e3f04cefbf089cf1d6670dc47c3fb4df68e2bad1fa5a369f9ce4b42bbd1210395a9d84d47d524548f79f435758c01faec5da2b7e551d3b8c995b7e06326ae4a52ae' it('should create a valid P2SH multisig scriptSig', function() { - var redeemScript = Script.createMultisigOutputScript(2, pubKeys) + var redeemScript = Script.createMultisigScriptPubKey(2, pubKeys) var actual = Script.createP2SHMultisigScriptSig(signatures, redeemScript) assert.equal(b2h(actual.buffer), expected) diff --git a/test/transaction.js b/test/transaction.js index 55f74e7..5716a48 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -260,7 +260,7 @@ describe('Transaction', function() { }) var pubKeys = privKeys.map(function(eck) { return eck.pub }) var pubKeyBuffers = pubKeys.map(function(q) { return q.toBuffer() }) - var redeemScript = Script.createMultisigOutputScript(2, pubKeyBuffers) + var redeemScript = Script.createMultisigScriptPubKey(2, pubKeyBuffers) var signatures = privKeys.map(function(privKey) { return tx.signScriptSig(0, redeemScript, privKey) From 5e0d38ba54a778c26962452f3e711c786011424c Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Mon, 5 May 2014 15:31:40 +1000 Subject: [PATCH 08/15] Address: add Address.*ScriptPubKey and tests The introduction of these two functions allow for the all the network related code to be eventually removed from Transaction and Script. Previously the result for non-standard transactions was undefined behaviour. This change mandates that an exception is thrown if a non-standard transaction is input. --- src/address.js | 55 ++++++++++++++++++++++++++++++++++++++++ src/script.js | 8 +----- test/address.js | 49 ++++++++++++++++++++++++++++++----- test/fixtures/address.js | 47 ++++++++++++++++++++++------------ 4 files changed, 129 insertions(+), 30 deletions(-) diff --git a/src/address.js b/src/address.js index 5457e52..bfe2f8f 100644 --- a/src/address.js +++ b/src/address.js @@ -1,5 +1,22 @@ var assert = require('assert') var base58check = require('./base58check') +var networks = require('./networks') + +function findScriptTypeByVersion(queryVersion) { + for (var networkName in networks) { + var network = networks[networkName] + + for (var versionName in network) { + var version = network[versionName] + + if (version === queryVersion) { + return versionName + } + } + } + + throw new Error('Version has no corresponding network') +} function Address(hash, version) { assert(Buffer.isBuffer(hash), 'First argument must be a Buffer') @@ -17,10 +34,48 @@ Address.fromBase58Check = function(string) { return new Address(decode.payload, decode.version) } +Address.fromScriptPubKey = function(script, network) { + network = network || networks.bitcoin + + var type = script.getOutType() + + // Pay-to-pubKeyHash + if (type === 'pubkeyhash') { + return new Address(new Buffer(script.chunks[2]), network.pubKeyHash) + } + + // Pay-to-scriptHash + else if (type === 'scripthash') { + return new Address(new Buffer(script.chunks[1]), network.scriptHash) + } + + throw new Error('Could not derive address from script') +} + // Export functions Address.prototype.toBase58Check = function () { return base58check.encode(this.hash, this.version) } + +Address.prototype.toScriptPubKey = function() { + var scriptType = findScriptTypeByVersion(this.version) + + // Pay-to-pubKeyHash + if (scriptType === 'pubKeyHash') { + return Script.createPubKeyHashScriptPubKey(this.hash) + } + + // Pay-to-scriptHash + else if (scriptType === 'scriptHash') { + return Script.createP2SHScriptPubKey(this.hash) + } + + throw new Error('Address has no matching script') +} + Address.prototype.toString = Address.prototype.toBase58Check module.exports = Address + +// http://stackoverflow.com/a/14098262 +var Script = require('./script') diff --git a/src/script.js b/src/script.js index 97aa070..f12ac6f 100644 --- a/src/script.js +++ b/src/script.js @@ -205,13 +205,7 @@ Script.prototype.toScriptHash = function() { Script.prototype.getToAddress = function(network) { network = network || networks.bitcoin - if(isPubkeyhash.call(this)) { - return new Address(new Buffer(this.chunks[2]), network.pubKeyHash) - } - - assert(isScripthash.call(this)) - - return new Address(new Buffer(this.chunks[1]), network.scriptHash) + return Address.fromScriptPubKey(this, network) } Script.prototype.getFromAddress = function(version) { diff --git a/test/address.js b/test/address.js index 25a541f..c276e87 100644 --- a/test/address.js +++ b/test/address.js @@ -1,15 +1,17 @@ var assert = require('assert') var Address = require('..').Address +var networks = require('..').networks +var Script = require('..').Script var b58fixtures = require('./fixtures/base58') var fixtures = require('./fixtures/address') -describe('Address', function() { - var bothVectors = fixtures.pubKeyHash.concat(fixtures.scriptHash) +function h2b(h) { return new Buffer(h, 'hex') } +describe('Address', function() { describe('Constructor', function() { it('does not mutate the input', function() { - bothVectors.forEach(function(f) { + fixtures.valid.forEach(function(f) { var hash = new Buffer(f.hex, 'hex') var addr = new Address(hash, f.version) @@ -28,8 +30,8 @@ describe('Address', function() { }) }) - bothVectors.forEach(function(f) { - it('imports ' + f.description + ' correctly', function() { + fixtures.valid.forEach(function(f) { + it('imports ' + f.description + '(' + f.network + ') correctly', function() { var addr = Address.fromBase58Check(f.base58check) assert.equal(addr.version, f.version) @@ -38,9 +40,21 @@ describe('Address', function() { }) }) + describe('fromScriptPubKey', function() { + fixtures.valid.forEach(function(f) { + it('imports ' + f.description + '(' + f.network + ') correctly', function() { + var script = Script.fromHex(f.script) + var addr = Address.fromScriptPubKey(script, networks[f.network]) + + assert.equal(addr.version, f.version) + assert.equal(addr.hash.toString('hex'), f.hex) + }) + }) + }) + describe('toBase58Check', function() { - bothVectors.forEach(function(f) { - it('exports ' + f.description + ' correctly', function() { + fixtures.valid.forEach(function(f) { + it('exports ' + f.description + '(' + f.network + ') correctly', function() { var addr = Address.fromBase58Check(f.base58check) var result = addr.toBase58Check() @@ -48,4 +62,25 @@ describe('Address', function() { }) }) }) + + describe('toScriptPubKey', function() { + fixtures.valid.forEach(function(f) { + it('imports ' + f.description + '(' + f.network + ') correctly', function() { + var addr = Address.fromBase58Check(f.base58check) + var script = addr.toScriptPubKey() + + assert.equal(script.toHex(), f.script) + }) + }) + + fixtures.invalid.toScriptPubKey.forEach(function(f) { + it('throws on ' + f.description, function() { + var addr = new Address(h2b(f.hex), f.version) + + assert.throws(function() { + addr.toScriptPubKey() + }) + }) + }) + }) }) diff --git a/test/fixtures/address.js b/test/fixtures/address.js index dd4fa28..7a6becc 100644 --- a/test/fixtures/address.js +++ b/test/fixtures/address.js @@ -1,30 +1,45 @@ module.exports = { - pubKeyHash: [ + valid: [ { - description: 'pubKeyHash (bitcoin)', + description: 'pubKeyHash', + network: 'bitcoin', version: 0, hex: '751e76e8199196d454941c45d1b3a323f1433bd6', - base58check: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH' + base58check: '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH', + script: '76a914751e76e8199196d454941c45d1b3a323f1433bd688ac' }, { - description: 'pubKeyHash (testnet)', + description: 'pubKeyHash', + network: 'testnet', version: 111, hex: '751e76e8199196d454941c45d1b3a323f1433bd6', - base58check: 'mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r' - } - ], - scriptHash: [ - { - description: 'scriptHash (bitcoin)', - version: 5, - hex: 'cd7b44d0b03f2d026d1e586d7ae18903b0d385f6', - base58check: '3LRW7jeCvQCRdPF8S3yUCfRAx4eqXFmdcr' + base58check: 'mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', + script: '76a914751e76e8199196d454941c45d1b3a323f1433bd688ac' }, { - description: 'scriptHash (testnet)', + description: 'scriptHash', + network: 'bitcoin', + version: 5, + hex: 'cd7b44d0b03f2d026d1e586d7ae18903b0d385f6', + base58check: '3LRW7jeCvQCRdPF8S3yUCfRAx4eqXFmdcr', + script: 'a914cd7b44d0b03f2d026d1e586d7ae18903b0d385f687' + }, + { + description: 'scriptHash', + network: 'testnet', version: 196, hex: 'cd7b44d0b03f2d026d1e586d7ae18903b0d385f6', - base58check: '2NByiBUaEXrhmqAsg7BbLpcQSAQs1EDwt5w' + base58check: '2NByiBUaEXrhmqAsg7BbLpcQSAQs1EDwt5w', + script: 'a914cd7b44d0b03f2d026d1e586d7ae18903b0d385f687' } - ] + ], + invalid: { + toScriptPubKey: [ + { + description: 'Unknown Address version', + version: 0x99, + hex: 'cd7b44d0b03f2d026d1e586d7ae18903b0d385f6' + } + ] + } } From 4207a0df99cb14018156aa31db4105e9586e6acc Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 6 May 2014 18:33:00 +1000 Subject: [PATCH 09/15] Address: use Address.Error instead This change is necessary for instanceof comparison when attempting to rethrow. Callbacks may be a better solution to introduce later. --- src/address.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/address.js b/src/address.js index bfe2f8f..405cb20 100644 --- a/src/address.js +++ b/src/address.js @@ -14,8 +14,6 @@ function findScriptTypeByVersion(queryVersion) { } } } - - throw new Error('Version has no corresponding network') } function Address(hash, version) { @@ -27,6 +25,13 @@ function Address(hash, version) { this.version = version } +Address.Error = function(message) { + this.name = 'AddressError' + this.message = message +} +Address.Error.prototype = new Error() +Address.Error.prototype.constructor = Address.Error + // Import functions Address.fromBase58Check = function(string) { var decode = base58check.decode(string) @@ -49,7 +54,7 @@ Address.fromScriptPubKey = function(script, network) { return new Address(new Buffer(script.chunks[1]), network.scriptHash) } - throw new Error('Could not derive address from script') + throw new Address.Error(type + ' has no matching Address') } // Export functions @@ -70,7 +75,7 @@ Address.prototype.toScriptPubKey = function() { return Script.createP2SHScriptPubKey(this.hash) } - throw new Error('Address has no matching script') + throw new Address.Error(this + ' has no matching script') } Address.prototype.toString = Address.prototype.toBase58Check From 708aa033901bb7463ad0d2272348f5fe63a40ca2 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 6 May 2014 16:52:31 +1000 Subject: [PATCH 10/15] Transaction/Script: bitcoin network no longer implied A Transaction (and its subsequent scripts) do not carry any network specific information in the Bitcoin protocol. Therefore they can not (without further context) produce the network specific constants for the generation of the base58 Addresses. As TransactionOut.address is used heavily throughout Wallet and other areas of the library, this could not be entirely removed without a large number of changes. For now, TransactionOut.address is only defined in the case of Tx.addOutput being used directly: Transaction.addOutput(address, value) --- src/script.js | 84 +-------------------------------------------- src/transaction.js | 27 ++++----------- src/wallet.js | 28 +++++++++------ test/script.js | 19 ---------- test/transaction.js | 4 +-- test/wallet.js | 27 ++++++++++----- 6 files changed, 47 insertions(+), 142 deletions(-) diff --git a/src/script.js b/src/script.js index f12ac6f..a0c0997 100644 --- a/src/script.js +++ b/src/script.js @@ -1,8 +1,6 @@ -var assert = require('assert') var Address = require('./address') +var assert = require('assert') var crypto = require('./crypto') -var convert = require('./convert') -var networks = require('./networks') var Opcode = require('./opcode') function Script(data) { @@ -202,18 +200,6 @@ Script.prototype.toScriptHash = function() { return crypto.hash160(this.buffer) } -Script.prototype.getToAddress = function(network) { - network = network || networks.bitcoin - - return Address.fromScriptPubKey(this, network) -} - -Script.prototype.getFromAddress = function(version) { - version = version || networks.bitcoin.pubKeyHash - - return new Address(this.simpleInHash(), version) -} - /** * Compare the script to known templates of scriptSig. * @@ -259,58 +245,6 @@ Script.prototype.getInType = function() { } } -/** - * Returns the affected public key for this input. - * - * This currently only works with payToPubKeyHash transactions. It will also - * work in the future for standard payToScriptHash transactions that use a - * single public key. - * - * However for multi-key and other complex transactions, this will only return - * one of the keys or raise an error. Therefore, it is recommended for indexing - * purposes to use Script#simpleInHash or Script#simpleOutHash instead. - * - * @deprecated - */ -Script.prototype.simpleInPubKey = function() { - switch (this.getInType()) { - case 'pubkeyhash': - return this.chunks[1] - case 'pubkey': - // TODO: Theoretically, we could recover the pubkey from the sig here. - // See https://bitcointalk.org/?topic=6430.0 - throw new Error('Script does not contain pubkey') - default: - throw new Error('Encountered non-standard scriptSig') - } -} - -/** - * Returns the affected address hash for this input. - * - * For standard transactions, this will return the hash of the pubKey that - * can spend this output. - * - * In the future, for standard payToScriptHash inputs, this will return the - * scriptHash. - * - * Note: This function provided for convenience. If you have the corresponding - * scriptPubKey available, you are urged to use Script#simpleOutHash instead - * as it is more reliable for non-standard payToScriptHash transactions. - * - * This method is useful for indexing transactions. - */ -Script.prototype.simpleInHash = function() { - return crypto.hash160(this.simpleInPubKey()) -} - -/** - * Old name for Script#simpleInHash. - * - * @deprecated - */ -Script.prototype.simpleInPubKeyHash = Script.prototype.simpleInHash - /** * Add an op code to the script. */ @@ -347,22 +281,6 @@ Script.prototype.writeBytes = function(data) { this.chunks.push(data) } -/** - * Create an output for an address - */ -Script.createScriptPubKey = function(address, network) { - assert(address instanceof Address) - network = network || networks.bitcoin - - if (address.version === network.pubKeyHash) { - return Script.createPubKeyHashScriptPubKey(address.hash) - } - - assert.strictEqual(address.version, network.scriptHash, 'Unknown address type') - - return Script.createP2SHScriptPubKey(address.hash) -} - // OP_DUP OP_HASH160 {pubKeyHash} OP_EQUALVERIFY OP_CHECKSIG Script.createPubKeyHashScriptPubKey = function(hash) { var script = new Script() diff --git a/src/transaction.js b/src/transaction.js index 49b1a4a..a0d77d0 100644 --- a/src/transaction.js +++ b/src/transaction.js @@ -9,7 +9,6 @@ var convert = require('./convert') var crypto = require('./crypto') var ECKey = require('./eckey').ECKey var ecdsa = require('./ecdsa') -var networks = require('./networks') var Transaction = function (doc) { if (!(this instanceof Transaction)) { return new Transaction(doc) } @@ -90,11 +89,10 @@ Transaction.prototype.addInput = function (tx, outIndex) { * i) An existing TransactionOut object * ii) An address object or a string address, and a value * iii) An address:value string - * iv) Either ii), iii) with an optional network argument * * FIXME: This is a bit convoluted */ -Transaction.prototype.addOutput = function (address, value, network) { +Transaction.prototype.addOutput = function (address, value) { if (arguments[0] instanceof TransactionOut) { this.outs.push(arguments[0]) return @@ -105,19 +103,15 @@ Transaction.prototype.addOutput = function (address, value, network) { var args = arguments[0].split(':') address = args[0] value = parseInt(args[1]) - - network = arguments[1] } address = Address.fromBase58Check(address) } - network = network || networks.bitcoin - this.outs.push(new TransactionOut({ value: value, - script: Script.createScriptPubKey(address, network), - network: network + script: address.toScriptPubKey(), + address: address // TODO: Remove me })) } @@ -364,18 +358,14 @@ Transaction.deserialize = function(buffer) { /** * Signs a standard output at some index with the given key - * FIXME: network support is ugly */ -Transaction.prototype.sign = function(index, key, type, network) { +Transaction.prototype.sign = function(index, key, type) { assert(key instanceof ECKey) - network = network || networks.bitcoin - var address = key.pub.getAddress(network.pubKeyHash) - - // FIXME: Assumed prior TX was pay-to-pubkey-hash - var script = Script.createScriptPubKey(address, network) + var script = key.pub.getAddress().toScriptPubKey() var signature = this.signScriptSig(index, script, key, type) + // FIXME: Assumed prior TX was pay-to-pubkey-hash var scriptSig = Script.createPubKeyHashScriptSig(signature, key.pub) this.setScriptSig(index, scriptSig) } @@ -445,10 +435,7 @@ function TransactionOut(data) { this.value = data.value this.address = data.address - var network = data.network || networks.bitcoin - if (this.script.buffer.length > 0) { - this.address = this.script.getToAddress(network) - } + if (data.address) this.address = data.address } TransactionOut.prototype.clone = function() { diff --git a/src/wallet.js b/src/wallet.js index c6b7134..08333c3 100644 --- a/src/wallet.js +++ b/src/wallet.js @@ -1,8 +1,9 @@ +var Address = require('./address') var convert = require('./convert') -var Transaction = require('./transaction').Transaction var HDNode = require('./hdwallet.js') -var rng = require('secure-random') var networks = require('./networks') +var rng = require('secure-random') +var Transaction = require('./transaction').Transaction function Wallet(seed, options) { if (!(this instanceof Wallet)) { return new Wallet(seed, options); } @@ -42,7 +43,6 @@ function Wallet(seed, options) { } this.newMasterKey(seed, network) - this.generateAddress = function() { var key = externalAccount.derive(this.addresses.length) this.addresses.push(key.getAddress().toString()) @@ -150,9 +150,17 @@ function Wallet(seed, options) { var txhash = tx.getHash() tx.outs.forEach(function(txOut, i){ - var address = txOut.address.toString() + var address + + try { + address = Address.fromScriptPubKey(txOut.script, networks[network]).toString() + } catch(e) { + if (!(e instanceof Address.Error)) throw e + } + if (isMyAddress(address)) { - var output = txhash+':'+i + var output = txhash + ':' + i + me.outputs[output] = { receive: output, value: txOut.value, @@ -165,7 +173,7 @@ function Wallet(seed, options) { var op = txIn.outpoint var o = me.outputs[op.hash+':'+op.index] if (o) { - o.spend = txhash+':'+i + o.spend = txhash + ':' +i } }) } @@ -174,7 +182,7 @@ function Wallet(seed, options) { checkDust(value) var tx = new Transaction() - tx.addOutput(to, value, networks[network]) + tx.addOutput(to, value) var utxo = getCandidateOutputs(value) var totalInValue = 0 @@ -190,7 +198,7 @@ function Wallet(seed, options) { var change = totalInValue - value - fee if(change > 0 && !isDust(change)) { - tx.addOutput(changeAddress || getChangeAddress(), change, networks[network]) + tx.addOutput(changeAddress || getChangeAddress(), change) } break } @@ -246,7 +254,7 @@ function Wallet(seed, options) { function estimateFeePadChangeOutput(tx){ var tmpTx = tx.clone() - tmpTx.addOutput(getChangeAddress(), 0, networks[network]) + tmpTx.addOutput(getChangeAddress(), 0) return tmpTx.estimateFee() } @@ -266,7 +274,7 @@ function Wallet(seed, options) { tx.ins.forEach(function(inp,i) { var output = me.outputs[inp.outpoint.hash + ':' + inp.outpoint.index] if (output) { - tx.sign(i, me.getPrivateKeyForAddress(output.address), false, networks[network]) + tx.sign(i, me.getPrivateKeyForAddress(output.address), false) } }) return tx diff --git a/test/script.js b/test/script.js index 303d181..83b35ae 100644 --- a/test/script.js +++ b/test/script.js @@ -116,25 +116,6 @@ describe('Script', function() { }) }) - describe('getToAddress', function() { - it('works for p2sh type output', function() { - var script = Script.fromHex(p2shScriptPubKey) - assert.equal(script.getToAddress().toString(), '3NukJ6fYZJ5Kk8bPjycAnruZkE5Q7UW7i8') - }) - - it('works for pubkey type output', function() { - var script = Script.fromHex(pubKeyScriptPubKey) - assert.equal(script.getToAddress().toString(), '19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') - }) - }) - - describe('getFromAddress', function() { - it('works for address type input', function() { - var script = Script.fromHex(addressScriptSig) - assert.equal(script.getFromAddress().toString(), '1BBjuhF2jHxu7tPinyQGCuaNhEs6f5u59u') - }) - }) - describe('2-of-3 Multi-Signature', function() { var pubKeys diff --git a/test/transaction.js b/test/transaction.js index 5716a48..e1ca2f4 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -176,7 +176,7 @@ describe('Transaction', function() { it('supports alternative networks', function(){ var addr = 'mkHJaNR7uuwRG1JrmTZsV4MszaTKjCBvCR' - tx.addOutput(addr, 40000, networks.testnet) + tx.addOutput(addr, 40000) verifyTransactionOut() assert.equal(tx.outs[0].address.toString(), addr) @@ -250,7 +250,7 @@ describe('Transaction', function() { it('works for multi-sig redeem script', function() { var tx = new Transaction() tx.addInput('d6f72aab8ff86ff6289842a0424319bf2ddba85dc7c52757912297f948286389', 0) - tx.addOutput('mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', 1, networks.testnet) + tx.addOutput('mrCDrCybB6J1vRfbwM5hemdJz73FwDBC8r', 1) var privKeys = [ '5HpHagT65TZzG1PH3CSu63k8DbpvD8s5ip4nEB3kEsreAnchuDf', diff --git a/test/wallet.js b/test/wallet.js index dd4d2ed..a7de4d1 100644 --- a/test/wallet.js +++ b/test/wallet.js @@ -1,12 +1,14 @@ -var Wallet = require('../src/wallet.js') +var assert = require('assert') +var crypto = require('../').crypto +var sinon = require('sinon') + +var Address = require('..').Address var HDNode = require('../src/hdwallet.js') var T = require('../src/transaction.js') var Transaction = T.Transaction var TransactionOut = T.TransactionOut var Script = require('../src/script.js') -var assert = require('assert') -var sinon = require('sinon') -var crypto = require('../').crypto +var Wallet = require('../src/wallet.js') var fixtureTxes = require('./fixtures/mainnet_tx') var fixtureTx1Hex = fixtureTxes.prevTx @@ -308,17 +310,24 @@ describe('Wallet', function() { }) describe('processTx', function(){ + var addresses var tx beforeEach(function(){ + addresses = [ + '115qa7iPZqn6as57hxLL8E9VUnhmGQxKWi', + '1Bu3bhwRmevHLAy1JrRB6AfcxfgDG2vXRd', + '1BBjuhF2jHxu7tPinyQGCuaNhEs6f5u59u' + ] + tx = Transaction.deserialize(fixtureTx1Hex) }) describe("when tx outs contains an address owned by the wallet, an 'output' gets added to wallet.outputs", function(){ it("works for receive address", function(){ var totalOuts = outputCount() - wallet.addresses = [tx.outs[0].address.toString()] + wallet.addresses = [addresses[0]] wallet.processTx(tx) assert.equal(outputCount(), totalOuts + 1) @@ -327,7 +336,7 @@ describe('Wallet', function() { it("works for change address", function(){ var totalOuts = outputCount() - wallet.changeAddresses = [tx.outs[1].address.toString()] + wallet.changeAddresses = [addresses[1]] wallet.processTx(tx) @@ -345,13 +354,15 @@ describe('Wallet', function() { var output = wallet.outputs[key] assert.equal(output.receive, key) assert.equal(output.value, txOut.value) - assert.equal(output.address, txOut.address) + + var txOutAddress = Address.fromScriptPubKey(txOut.script).toString() + assert.equal(output.address, txOutAddress) } }) describe("when tx ins outpoint contains a known txhash:i, the corresponding 'output' gets updated", function(){ beforeEach(function(){ - wallet.addresses = [tx.outs[0].address.toString()] // the address fixtureTx2 used as input + wallet.addresses = [addresses[0]] // the address fixtureTx2 used as input wallet.processTx(tx) tx = Transaction.deserialize(fixtureTx2Hex) From 48e15b587a1ca53df60037d06b7068f9ea9a10d1 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 9 May 2014 12:25:24 +1000 Subject: [PATCH 11/15] Address: remove redundant comments --- src/address.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/address.js b/src/address.js index 405cb20..7c7650d 100644 --- a/src/address.js +++ b/src/address.js @@ -44,12 +44,10 @@ Address.fromScriptPubKey = function(script, network) { var type = script.getOutType() - // Pay-to-pubKeyHash if (type === 'pubkeyhash') { return new Address(new Buffer(script.chunks[2]), network.pubKeyHash) } - // Pay-to-scriptHash else if (type === 'scripthash') { return new Address(new Buffer(script.chunks[1]), network.scriptHash) } @@ -65,12 +63,10 @@ Address.prototype.toBase58Check = function () { Address.prototype.toScriptPubKey = function() { var scriptType = findScriptTypeByVersion(this.version) - // Pay-to-pubKeyHash if (scriptType === 'pubKeyHash') { return Script.createPubKeyHashScriptPubKey(this.hash) } - // Pay-to-scriptHash else if (scriptType === 'scriptHash') { return Script.createP2SHScriptPubKey(this.hash) } From 342c55c8640ee17c822d3a1f0af1d1c61f8fd43b Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 9 May 2014 12:26:38 +1000 Subject: [PATCH 12/15] Address: remove circular dependency fix This is no longer a problem. --- src/address.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/address.js b/src/address.js index 7c7650d..ded547e 100644 --- a/src/address.js +++ b/src/address.js @@ -1,6 +1,7 @@ var assert = require('assert') var base58check = require('./base58check') var networks = require('./networks') +var Script = require('./script') function findScriptTypeByVersion(queryVersion) { for (var networkName in networks) { @@ -77,6 +78,3 @@ Address.prototype.toScriptPubKey = function() { Address.prototype.toString = Address.prototype.toBase58Check module.exports = Address - -// http://stackoverflow.com/a/14098262 -var Script = require('./script') From cace8316e4433d61c32a5d262f54cf1b6eff91f4 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 9 May 2014 12:56:35 +1000 Subject: [PATCH 13/15] Script: extract test data to fixture --- test/fixtures/script.js | 54 ++++++++++++++++++++++++ test/script.js | 93 ++++++++++++----------------------------- 2 files changed, 81 insertions(+), 66 deletions(-) create mode 100644 test/fixtures/script.js diff --git a/test/fixtures/script.js b/test/fixtures/script.js new file mode 100644 index 0000000..8fb5d21 --- /dev/null +++ b/test/fixtures/script.js @@ -0,0 +1,54 @@ +module.exports = { + valid: [ + { + description: 'P2SH ScriptPubKey', + hex: 'a914e8c300c87986efa84c37c0519929019ef86eb5b487', + type: 'scripthash', + hash: '0ba47b56a573bab4b430ad6ed3ec79270e04b066', + scriptPubKey: true + }, + { + description: 'PubKeyHash ScriptPubKey', + hex: '76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac', + type: 'pubkeyhash', + hash: 'a5313f33d5c7b81674b35f7f3febc3522ef234db', + scriptPubKey: true + }, + { + description: 'pubKeyHash scriptSig', + hex: '48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8', + type: 'pubkeyhash', + hash: 'b9bac2a5c5c29bb27c382d41fa3d179c646c78fd', + scriptPubKey: false + }, + { + description: 'Valid multisig script', + hex: '5121032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca330162102308673d16987eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a52ae', + type: 'multisig', + hash: 'f1c98f0b74ecabcf78ae20dfa224bb6666051fbe', + scriptPubKey: true + }, + { + description: 'OP_RETURN script', + hex:'6a2606deadbeef03f895a2ad89fb6d696497af486cb7c644a27aa568c7a18dd06113401115185474', + type: 'nulldata', + hash: 'ec88f016655477663455fe6a8e83508c348ea145', + scriptPubKey: true + }, + { + description: 'Non standard script', + hex: 'aa206fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000087', + type: 'nonstandard', + hash: '3823382e70d1930989813d3459988e0d7c2861d8', + scriptPubKey: true + }, + { + description: 'Invalid multisig script', + asm: '0 0 0 OP_CHECKmulTISIG', + hex: '000000ae', + type: 'nonstandard', + hash: '62ede8963f9387544935f168745262f703dab1fb', + scriptPubKey: true + } + ] +} diff --git a/test/script.js b/test/script.js index 83b35ae..1da5fc1 100644 --- a/test/script.js +++ b/test/script.js @@ -5,30 +5,12 @@ var networks = require('..').networks var Address = require('../src/address.js') var Script = require('../src/script.js') +var fixtures = require('./fixtures/script') + function b2h(b) { return new Buffer(b).toString('hex') } function h2b(h) { return new Buffer(h, 'hex') } describe('Script', function() { - var p2shScriptPubKey, pubKeyScriptPubKey, addressScriptSig - - beforeEach(function(){ - p2shScriptPubKey = "a914e8c300c87986efa84c37c0519929019ef86eb5b487" - pubKeyScriptPubKey = "76a9145a3acbc7bbcc97c5ff16f5909c9d7d3fadb293a888ac" - addressScriptSig = "48304502206becda98cecf7a545d1a640221438ff8912d9b505ede67e0138485111099f696022100ccd616072501310acba10feb97cecc918e21c8e92760cd35144efec7622938f30141040cd2d2ce17a1e9b2b3b2cb294d40eecf305a25b7e7bfdafae6bb2639f4ee399b3637706c3d377ec4ab781355add443ae864b134c5e523001c442186ea60f0eb8" - - // txid: 09dd94f2c85262173da87a745a459007bb1eed6eeb6bfa238a0cd91a16cf7790 - validMultisigScript = '5121032487c2a32f7c8d57d2a93906a6457afd00697925b0e6e145d89af6d3bca330162102308673d16987eaa010e540901cc6fe3695e758c19f46ce604e174dac315e685a52ae' - - // txid: 5e9be7fb36ee49ce84bee4c8ef38ad0efc0608b78dae1c2c99075297ef527890 - opreturnScript = '6a2606deadbeef03f895a2ad89fb6d696497af486cb7c644a27aa568c7a18dd06113401115185474' - - // asm: "0 0 0 OP_CHECKMULTISIG" - invalidMultisigScript = '000000ae' - - // txid: a4bfa8ab6435ae5f25dae9d89e4eb67dfa94283ca751f393c1ddc5a837bbc31b - nonStandardScript = 'aa206fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000087' - }) - describe('constructor', function() { it('works for a byte array', function() { assert.ok(new Script([])) @@ -44,57 +26,34 @@ describe('Script', function() { }) describe('fromHex/toHex', function() { - it('matches the test data', function() { - [ - p2shScriptPubKey, - pubKeyScriptPubKey, - addressScriptSig, - validMultisigScript, - opreturnScript, - nonStandardScript, - invalidMultisigScript - ].forEach(function(hex) { - assert.equal(Script.fromHex(hex).toHex(), hex) + it('matches the test vectors', function() { + fixtures.valid.forEach(function(f) { + assert.equal(Script.fromHex(f.hex).toHex(), f.hex) }) }) }) - describe('getOutType', function() { - it('supports p2sh', function() { - var script = Script.fromHex(p2shScriptPubKey) - assert.equal(script.getOutType(), 'scripthash') - }) + describe('getInType', function() { + fixtures.valid.forEach(function(f) { + if (!f.scriptPubKey) { + it('supports ' + f.description, function() { + var script = Script.fromHex(f.hex) - it('supports pubkeyhash', function() { - var script = Script.fromHex(pubKeyScriptPubKey) - assert.equal(script.getOutType(), 'pubkeyhash') - }) - - it('supports multisig', function() { - var script = Script.fromHex(validMultisigScript) - assert.equal(script.getOutType(), 'multisig') - }) - - it('supports null_data', function() { - var script = Script.fromHex(opreturnScript) - assert.equal(script.getOutType(), 'nulldata') - }) - - it('supports nonstandard script', function() { - var script = Script.fromHex(nonStandardScript) - assert.equal(script.getOutType(), 'nonstandard') - }) - - it('identifies invalid multisig script as nonstandard', function() { - var script = Script.fromHex(invalidMultisigScript) - assert.equal(script.getOutType(), 'nonstandard') + assert.equal(script.getInType(), f.type) + }) + } }) }) - describe('getInType', function() { - it('works for address', function() { - var script = Script.fromHex(addressScriptSig) - assert.equal(script.getInType(), 'pubkeyhash') + describe('getOutType', function() { + fixtures.valid.forEach(function(f) { + if (f.scriptPubKey) { + it('supports ' + f.description, function() { + var script = Script.fromHex(f.hex) + + assert.equal(script.getOutType(), f.type) + }) + } }) }) @@ -103,7 +62,8 @@ describe('Script', function() { var address = Address.fromBase58Check('19E6FV3m3kEPoJD5Jz6dGKdKwTVvjsWUvu') var script = Script.createPubKeyHashScriptPubKey(address.hash) - assert.equal(script.toHex(), pubKeyScriptPubKey) + // FIXME: not good TDD + assert.equal(script.toHex(), fixtures.valid[1].hex) }) }) @@ -112,11 +72,12 @@ describe('Script', function() { var hash = new Buffer('e8c300c87986efa84c37c0519929019ef86eb5b4', 'hex') var script = Script.createP2SHScriptPubKey(hash) - assert.equal(script.toHex(), p2shScriptPubKey) + // FIXME: not good TDD + assert.equal(script.toHex(), fixtures.valid[0].hex) }) }) - describe('2-of-3 Multi-Signature', function() { + describe('2-of-3 Multi-Signature scriptPubKey', function() { var pubKeys beforeEach(function() { From f8e662e4955639a3582d6c1d88c250b5fedf8134 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 9 May 2014 12:56:51 +1000 Subject: [PATCH 14/15] Script: rename toScriptHash to getHash and add tests --- src/script.js | 14 +------------- test/script.js | 12 +++++++++++- test/transaction.js | 4 +--- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/script.js b/src/script.js index a0c0997..2c6fbea 100644 --- a/src/script.js +++ b/src/script.js @@ -184,19 +184,7 @@ function isSmallIntOp(opcode) { ((opcode >= Opcode.map.OP_1) && (opcode <= Opcode.map.OP_16))) } -/** - * Returns the address corresponding to this output in hash160 form. - * Assumes strange scripts are P2SH - */ -Script.prototype.toScriptHash = function() { - if(isPubkeyhash.call(this)) { - return this.chunks[2] - } - - if(isScripthash.call(this)) { - return crypto.hash160(this.buffer) - } - +Script.prototype.getHash = function() { return crypto.hash160(this.buffer) } diff --git a/test/script.js b/test/script.js index 1da5fc1..faf4bb8 100644 --- a/test/script.js +++ b/test/script.js @@ -26,9 +26,19 @@ describe('Script', function() { }) describe('fromHex/toHex', function() { + fixtures.valid.forEach(function(f) { + it('decodes/encodes ' + f.description, function() { + assert.equal(Script.fromHex(f.hex).toHex(), f.hex) + }) + }) + }) + + describe('getHash', function() { it('matches the test vectors', function() { fixtures.valid.forEach(function(f) { - assert.equal(Script.fromHex(f.hex).toHex(), f.hex) + var script = Script.fromHex(f.hex) + + assert.equal(script.getHash().toString('hex'), f.hash) }) }) }) diff --git a/test/transaction.js b/test/transaction.js index e1ca2f4..62e0525 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -72,9 +72,7 @@ describe('Transaction', function() { var output = tx.outs[0] assert.equal(output.value, 5000000000) - assert.equal(b2h(output.script.toScriptHash()), "dd40dedd8f7e37466624c4dacc6362d8e7be23dd") - // assert.equal(output.address.toString(), "n1gqLjZbRH1biT5o4qiVMiNig8wcCPQeB9") - // TODO: address is wrong because it's a testnet transaction. Transaction needs to support testnet + assert.deepEqual(output.script, Address.fromBase58Check('n1gqLjZbRH1biT5o4qiVMiNig8wcCPQeB9').toScriptPubKey()) }) it('assigns hash to deserialized object', function(){ From f18c5e4c1ee64d46bd7e5ebf2070310360c142b7 Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Fri, 9 May 2014 16:18:35 +1000 Subject: [PATCH 15/15] Script: removes Script.createP2SHMultiSigScriptSig It is favoured to compose the scriptSig manually using Script.createP2SHScriptSig and Script.createMultisigScriptSig. Added a test to verify that createMultisigScriptSig throws when not enough signatures a provided and the redeemScript is given. --- src/script.js | 22 +++++++++------------- test/script.js | 14 ++++++++++++-- test/transaction.js | 3 ++- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/script.js b/src/script.js index 2c6fbea..6add3f1 100644 --- a/src/script.js +++ b/src/script.js @@ -317,7 +317,15 @@ Script.createPubKeyHashScriptSig = function(signature, pubKey) { } // OP_0 [signatures ...] -Script.createMultisigScriptSig = function(signatures) { +Script.createMultisigScriptSig = function(signatures, scriptPubKey) { + if (scriptPubKey) { + assert(isMultisig.call(scriptPubKey)) + + var m = scriptPubKey.chunks[0] + var k = m - (Opcode.map.OP_1 - 1) + assert(k <= signatures.length, 'Not enough signatures provided') + } + var inScript = new Script() inScript.writeOp(Opcode.map.OP_0) @@ -335,18 +343,6 @@ Script.createP2SHScriptSig = function(scriptSig, scriptPubKey) { return inScript } -// [signatures ...] {m [pubKeys ...] n OP_CHECKSIG} -Script.createP2SHMultisigScriptSig = function(signatures, scriptPubKey) { - assert(isMultisig.call(scriptPubKey)) - - var m = scriptPubKey.chunks[0] - var k = m - (Opcode.map.OP_1 - 1) - assert(k <= signatures.length, 'Not enough signatures provided') - - var scriptSig = Script.createMultisigScriptSig(signatures) - return Script.createP2SHScriptSig(scriptSig, scriptPubKey) -} - Script.prototype.clone = function() { return new Script(this.buffer) } diff --git a/test/script.js b/test/script.js index faf4bb8..218e971 100644 --- a/test/script.js +++ b/test/script.js @@ -121,9 +121,19 @@ describe('Script', function() { it('should create a valid P2SH multisig scriptSig', function() { var redeemScript = Script.createMultisigScriptPubKey(2, pubKeys) - var actual = Script.createP2SHMultisigScriptSig(signatures, redeemScript) + var redeemScriptSig = Script.createMultisigScriptSig(signatures) - assert.equal(b2h(actual.buffer), expected) + var scriptSig = Script.createP2SHScriptSig(redeemScriptSig, redeemScript) + + assert.equal(b2h(scriptSig.buffer), expected) + }) + + it('should throw on not enough signatures', function() { + var redeemScript = Script.createMultisigScriptPubKey(2, pubKeys) + + assert.throws(function() { + Script.createMultisigScriptSig(signatures.slice(1), redeemScript) + }) }) }) }) diff --git a/test/transaction.js b/test/transaction.js index 62e0525..d4e1e13 100644 --- a/test/transaction.js +++ b/test/transaction.js @@ -264,7 +264,8 @@ describe('Transaction', function() { return tx.signScriptSig(0, redeemScript, privKey) }) - var scriptSig = Script.createP2SHMultisigScriptSig(signatures, redeemScript) + var redeemScriptSig = Script.createMultisigScriptSig(signatures) + var scriptSig = Script.createP2SHScriptSig(redeemScriptSig, redeemScript) tx.setScriptSig(0, scriptSig) signatures.forEach(function(sig, i){