diff --git a/lib/address.js b/lib/address.js index 248b5cb..c85522e 100644 --- a/lib/address.js +++ b/lib/address.js @@ -59,7 +59,7 @@ function Address(data, network, type) { } else if (data.constructor && (data.constructor.name && data.constructor.name === 'PublicKey')) { info = Address._transformPublicKey(data); } else if (data.constructor && (data.constructor.name && data.constructor.name === 'Script')) { - info = Address._transformScript(data); + info = Address._transformScript(data, network); } else if (data instanceof Address) { return data; } else if (typeof(data) === 'string') { @@ -204,11 +204,12 @@ Address._transformPublicKey = function(pubkey){ * @returns {Object} An object with keys: hashBuffer, type * @private */ -Address._transformScript = function(script){ +Address._transformScript = function(script, network){ var info = {}; if (!script.constructor || (script.constructor.name && script.constructor.name !== 'Script')) { throw new TypeError('Address must be an instance of Script.'); } + info.network = network || Networks.defaultNetwork; info.hashBuffer = Hash.sha256ripemd160(script.toBuffer()); info.type = Address.PayToScriptHash; return info; @@ -277,7 +278,7 @@ Address.fromScriptHash = function(hash, network) { * @returns {Address} A new valid and frozen instance of an Address */ Address.fromScript = function(script, network) { - var info = Address._transformScript(script); + var info = Address._transformScript(script, network); return new Address(info.hashBuffer, network, info.type); }; diff --git a/lib/crypto/ecdsa.js b/lib/crypto/ecdsa.js index a5e14eb..78a62c1 100644 --- a/lib/crypto/ecdsa.js +++ b/lib/crypto/ecdsa.js @@ -1,6 +1,7 @@ 'use strict'; var BN = require('./bn'); +var BufferReader = require('../encoding/bufferreader'); var Point = require('./point'); var Signature = require('./signature'); var PublicKey = require('../publickey'); diff --git a/lib/encoding/bufferreader.js b/lib/encoding/bufferreader.js index 1f51f53..31e3424 100644 --- a/lib/encoding/bufferreader.js +++ b/lib/encoding/bufferreader.js @@ -38,6 +38,12 @@ BufferReader.prototype.read = function(len) { return buf; }; +BufferReader.prototype.readAll = function() { + var buf = this.buf.slice(this.pos, this.buf.length); + this.pos = this.buf.length; + return buf; +}; + BufferReader.prototype.readUInt8 = function() { var val = this.buf.readUInt8(this.pos); this.pos = this.pos + 1; diff --git a/lib/errors/spec.js b/lib/errors/spec.js index c011378..b035f0f 100644 --- a/lib/errors/spec.js +++ b/lib/errors/spec.js @@ -34,6 +34,13 @@ module.exports = [{ }, { name: 'InvalidArgumentType', message: format('Invalid Argument for {2}, expected {1} but got ') + '+ typeof arguments[0]', + }, { + name: 'Script', + message: format('Internal Error on Script {0}'), + errors: [{ + name: 'UnrecognizedAddress', + message: format('Expected argument {0} to be an address') + }] }, { name: 'HDPrivateKey', message: format('Internal Error on HDPrivateKey {0}'), diff --git a/lib/privatekey.js b/lib/privatekey.js index 6f4cae7..52544a8 100644 --- a/lib/privatekey.js +++ b/lib/privatekey.js @@ -38,6 +38,9 @@ var PrivateKey = function PrivateKey(data, network, compressed) { if (!(this instanceof PrivateKey)) { return new PrivateKey(data, network, compressed); } + if (data instanceof PrivateKey) { + return data; + } var info = { compressed: typeof(compressed) !== 'undefined' ? compressed : true, diff --git a/lib/script.js b/lib/script.js index e14f982..de9e578 100644 --- a/lib/script.js +++ b/lib/script.js @@ -11,8 +11,9 @@ var Signature = require('./crypto/signature'); var $ = require('./util/preconditions'); var _ = require('lodash'); +var errors = require('./errors'); var buffer = require('buffer'); -var bufferUtil = require('./util/buffer'); +var BufferUtil = require('./util/buffer'); var jsUtil = require('./util/js'); /** @@ -31,8 +32,10 @@ var Script = function Script(from) { this.chunks = []; - if (bufferUtil.isBuffer(from)) { + if (BufferUtil.isBuffer(from)) { return Script.fromBuffer(from); + } else if (from instanceof Address) { + return Script.fromAddress(from); } else if (from instanceof Script) { return Script.fromBuffer(from.toBuffer()); } else if (typeof from === 'string') { @@ -236,7 +239,7 @@ Script.prototype.getPublicKeyHash = function() { */ Script.prototype.isPublicKeyOut = function() { return this.chunks.length === 2 && - bufferUtil.isBuffer(this.chunks[0].buf) && + BufferUtil.isBuffer(this.chunks[0].buf) && PublicKey.isValid(this.chunks[0].buf) && this.chunks[1].opcodenum === Opcode.OP_CHECKSIG; }; @@ -246,7 +249,7 @@ Script.prototype.isPublicKeyOut = function() { */ Script.prototype.isPublicKeyIn = function() { return this.chunks.length === 1 && - bufferUtil.isBuffer(this.chunks[0].buf) && + BufferUtil.isBuffer(this.chunks[0].buf) && this.chunks[0].buf.length === 0x47; }; @@ -290,7 +293,7 @@ Script.prototype.isMultisigOut = function() { return (this.chunks.length > 3 && Opcode.isSmallIntOp(this.chunks[0].opcodenum) && this.chunks.slice(1, this.chunks.length - 2).every(function(obj) { - return obj.buf && bufferUtil.isBuffer(obj.buf); + return obj.buf && BufferUtil.isBuffer(obj.buf); }) && Opcode.isSmallIntOp(this.chunks[this.chunks.length - 2].opcodenum) && this.chunks[this.chunks.length - 1].opcodenum === Opcode.OP_CHECKMULTISIG); @@ -305,7 +308,7 @@ Script.prototype.isMultisigIn = function() { this.chunks[0].opcodenum === 0 && this.chunks.slice(1, this.chunks.length).every(function(obj) { return obj.buf && - bufferUtil.isBuffer(obj.buf) && + BufferUtil.isBuffer(obj.buf) && obj.buf.length === 0x47; }); }; @@ -392,6 +395,30 @@ Script.prototype.prepend = function(obj) { return this; }; +/** + * Compares a script with another script + */ +Script.prototype.equals = function(script) { + $.checkState(script instanceof Script, 'Must provide another script'); + if (this.chunks.length !== script.chunks.length) { + return false; + } + var i; + for (i = 0; i < this.chunks.length; i++) { + if (BufferUtil.isBuffer(this.chunks[i]) && !BufferUtil.isBuffer(script.chunks[i])) { + return false; + } else if (this.chunks[i] instanceof Opcode && !(script.chunks[i] instanceof Opcode)) { + return false; + } + if (BufferUtil.isBuffer(this.chunks[i]) && !BufferUtil.equals(this.chunks[i], script.chunks[i])) { + return false; + } else if (this.chunks[i].num !== script.chunks[i].num) { + return false; + } + } + return true; +}; + /** * Adds a script element to the end of the script. * @@ -411,10 +438,12 @@ Script.prototype._addByType = function(obj, prepend) { this._addOpcode(obj, prepend); } else if (obj.constructor && obj.constructor.name && obj.constructor.name === 'Opcode') { this._addOpcode(obj, prepend); - } else if (bufferUtil.isBuffer(obj)) { + } else if (BufferUtil.isBuffer(obj)) { this._addBuffer(obj, prepend); } else if (typeof obj === 'object') { this._insertAtPosition(obj, prepend); + } else if (obj instanceof Script) { + this.chunks = this.chunks.concat(obj.chunks); } else { throw new Error('Invalid script chunk'); } @@ -493,6 +522,7 @@ Script.buildMultisigOut = function(pubkeys, m, opts) { opts = opts || {}; var s = new Script(); s.add(Opcode.smallInt(m)); + pubkeys = _.map(pubkeys, function(pubkey) { return PublicKey(pubkey); }); var sorted = pubkeys; if (!opts.noSorting) { sorted = _.sortBy(pubkeys, function(pubkey) { @@ -508,6 +538,29 @@ Script.buildMultisigOut = function(pubkeys, m, opts) { return s; }; +/** + * A new P2SH Multisig input script for the given public keys, requiring m of those public keys to spend + * + * @param {PublicKey[]} pubkeys list of all public keys controlling the output + * @param {number} threshold amount of required signatures to spend the output + * @param {Array} signatures signatures to append to the script + * @param {Object=} opts + * @param {boolean=false} opts.noSorting don't sort the given public keys before creating the script + * @param {Script=} opts.cachedMultisig don't recalculate the redeemScript + * + * @returns Script + */ +Script.buildP2SHMultisigIn = function(pubkeys, threshold, signatures, opts) { + opts = opts || {}; + var s = new Script(); + s.add(Opcode.OP_0); + _.each(signatures, function(signature) { + s.add(signature); + }); + s.add((opts.cachedMultisig || Script.buildMultisigOut(pubkeys, threshold, opts)).toBuffer()); + return s; +}; + /** * @returns a new pay to public key hash output for the given * address or public key @@ -575,9 +628,9 @@ Script.buildScriptHashOut = function(script) { */ Script.buildPublicKeyHashIn = function(publicKey, signature, sigtype) { var script = new Script() - .add(bufferUtil.concat([ + .add(BufferUtil.concat([ signature, - bufferUtil.integerAsSingleByteBuffer(sigtype || Signature.SIGHASH_ALL) + BufferUtil.integerAsSingleByteBuffer(sigtype || Signature.SIGHASH_ALL) ])) .add(new PublicKey(publicKey).toBuffer()); return script; @@ -597,4 +650,17 @@ Script.prototype.toScriptHashOut = function() { return Script.buildScriptHashOut(this); }; +/** + * @return Script a script built from the address + */ +Script.fromAddress = function(address) { + address = Address(address); + if (address.isPayToScriptHash()) { + return Script.buildScriptHashOut(address); + } else if (address.isPayToPublicKeyHash()) { + return Script.buildPublicKeyHashOut(address); + } + throw new errors.Script.UnrecognizedAddress(address); +}; + module.exports = Script; diff --git a/lib/transaction/input/index.js b/lib/transaction/input/index.js index ee83cb8..9103d32 100644 --- a/lib/transaction/input/index.js +++ b/lib/transaction/input/index.js @@ -1,8 +1,4 @@ -'use strict'; +module.exports = require('./input'); -var Input = require('./input'); - -Input.PublicKeyHash = require('./publicKeyHash'); -Input.ScriptHash = require('./scriptHash'); - -module.exports = Input; +module.exports.PublicKeyHash = require('./publickeyhash'); +module.exports.MultiSigScriptHash = require('./multisigscripthash.js'); diff --git a/lib/transaction/input/input.js b/lib/transaction/input/input.js index ad24ca4..23b6ef9 100644 --- a/lib/transaction/input/input.js +++ b/lib/transaction/input/input.js @@ -7,6 +7,7 @@ var buffer = require('buffer'); var bufferUtil = require('../../util/buffer'); var JSUtil = require('../../util/js'); var Script = require('../../script'); +var Sighash = require('../sighash'); function Input(params) { if (!(this instanceof Input)) { @@ -123,4 +124,24 @@ Input.prototype.getSignatures = function() { throw new errors.AbstractMethodInvoked('Input#getSignatures'); }; +Input.prototype.isFullySigned = function() { + throw new errors.AbstractMethodInvoked('Input#isFullySigned'); +}; + +Input.prototype.addSignature = function() { + throw new errors.AbstractMethodInvoked('Input#addSignature'); +}; + +Input.prototype.isValidSignature = function(transaction, signature) { + // FIXME: Refactor signature so this is not necessary + signature.signature.nhashtype = signature.sigtype; + return Sighash.verify( + transaction, + signature.signature, + signature.publicKey, + signature.inputIndex, + this.output.script + ); +}; + module.exports = Input; diff --git a/lib/transaction/input/multisigscripthash.js b/lib/transaction/input/multisigscripthash.js new file mode 100644 index 0000000..f876b3d --- /dev/null +++ b/lib/transaction/input/multisigscripthash.js @@ -0,0 +1,108 @@ +'use strict'; + +var _ = require('lodash'); +var JSUtil = require('../../util/js'); +var inherits = require('inherits'); +var Input = require('./input'); +var Output = require('../output'); +var $ = require('../../util/preconditions'); + +var Script = require('../../script'); +var Signature = require('../../crypto/signature'); +var Sighash = require('../sighash'); +var BufferUtil = require('../../util/buffer'); + +function MultiSigScriptHashInput(input, pubkeys, threshold) { + Input.apply(this, arguments); + var self = this; + this.publicKeys = _.sortBy(pubkeys, function(publicKey) { return publicKey.toString('hex'); }); + this.redeemScript = Script.buildMultisigOut(this.publicKeys, threshold); + $.checkState(Script.buildScriptHashOut(this.redeemScript).equals(this.output.script), + 'Provided public keys don\'t hash to the provided output'); + this.publicKeyIndex = {}; + _.each(this.publicKeys, function(publicKey, index) { + self.publicKeyIndex[publicKey.toString()] = index; + }); + this.threshold = threshold; + // Empty array of signatures + this.signatures = new Array(this.publicKeys.length); +} +inherits(MultiSigScriptHashInput, Input); + +MultiSigScriptHashInput.prototype.getSignatures = function(transaction, privateKey, index, sigtype) { + $.checkState(this.output instanceof Output); + sigtype = sigtype || Signature.SIGHASH_ALL; + + var self = this; + var results = []; + _.each(this.publicKeys, function(publicKey) { + if (publicKey.toString() === privateKey.publicKey.toString()) { + results.push({ + publicKey: privateKey.publicKey, + prevTxId: self.txId, + outputIndex: self.outputIndex, + inputIndex: index, + signature: Sighash.sign(transaction, privateKey, sigtype, index, self.redeemScript), + sigtype: sigtype + }); + } + }); + return results; +}; + +MultiSigScriptHashInput.prototype.addSignature = function(transaction, signature) { + $.checkState(!this.isFullySigned(), 'All needed signatures have already been added'); + $.checkArgument(!_.isUndefined(this.publicKeyIndex[signature.publicKey.toString()]), + 'Signature has no matching public key'); + $.checkState(this.isValidSignature(transaction, signature)); + this.signatures[this.publicKeyIndex[signature.publicKey.toString()]] = signature; + this._updateScript(); + return this; +}; + +MultiSigScriptHashInput.prototype._updateScript = function() { + this.setScript(Script.buildP2SHMultisigIn( + this.publicKeys, + this.threshold, + this._createSignatures(), + { cachedMultisig: this.redeemScript } + )); + return this; +}; + +MultiSigScriptHashInput.prototype._createSignatures = function() { + var reverseOrder = JSUtil.cloneArray(this.signatures).reverse(); + return _.map( + _.filter(reverseOrder, function(signature) { return !_.isUndefined(signature); }), + function(signature) { + return BufferUtil.concat([ + signature.signature.toDER(), + BufferUtil.integerAsSingleByteBuffer(signature.sigtype) + ]); + } + ); +}; + +MultiSigScriptHashInput.prototype.clearSignatures = function() { + this.signatures = new Array(this.publicKeys.length); + this._updateScript(); +}; + +MultiSigScriptHashInput.prototype.isFullySigned = function() { + var count = _.reduce(this.signatures, function(sum, signature) { return sum + (!!signature); }, 0); + return count === this.threshold; +}; + +MultiSigScriptHashInput.prototype.isValidSignature = function(transaction, signature) { + // FIXME: Refactor signature so this is not necessary + signature.signature.nhashtype = signature.sigtype; + return Sighash.verify( + transaction, + signature.signature, + signature.publicKey, + signature.inputIndex, + this.redeemScript + ); +}; + +module.exports = MultiSigScriptHashInput; diff --git a/lib/transaction/input/publicKeyHash.js b/lib/transaction/input/publickeyhash.js similarity index 93% rename from lib/transaction/input/publicKeyHash.js rename to lib/transaction/input/publickeyhash.js index a94b75c..f43400a 100644 --- a/lib/transaction/input/publicKeyHash.js +++ b/lib/transaction/input/publickeyhash.js @@ -57,7 +57,8 @@ PublicKeyHashInput.prototype.getSignatures = function(transaction, privateKey, i * @param {number=Signature.SIGHASH_ALL} signature.sigtype * @return {PublicKeyHashInput} this, for chaining */ -PublicKeyHashInput.prototype.addSignature = function(signature) { +PublicKeyHashInput.prototype.addSignature = function(transaction, signature) { + $.checkState(this.isValidSignature(transaction, signature), 'Signature is invalid'); this.setScript(Script.buildPublicKeyHashIn( signature.publicKey, signature.signature.toDER(), diff --git a/lib/transaction/input/scriptHash.js b/lib/transaction/input/scriptHash.js deleted file mode 100644 index ad7cfae..0000000 --- a/lib/transaction/input/scriptHash.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var inherits = require('inherits'); -var Input = require('./input'); -var Hash = require('../../crypto/hash'); -var Signature = require('../../crypto/signature'); -var Sighash = require('../sighash'); -var BufferUtil = require('../../util/buffer'); - -function ScriptHashInput() { - Input.apply(this, arguments); -} -inherits(ScriptHashInput, Input); - -ScriptHashInput.prototype.getSignatures = function(transaction, privateKey, index, sigtype, hashData) { - return []; -}; - -module.exports = ScriptHashInput; diff --git a/lib/transaction/sighash.js b/lib/transaction/sighash.js index fd0c78c..3d8eca6 100644 --- a/lib/transaction/sighash.js +++ b/lib/transaction/sighash.js @@ -4,7 +4,6 @@ var buffer = require('buffer'); var Signature = require('../crypto/signature'); var Script = require('../script'); -var Input = require('./input'); var Output = require('./output'); var BufferReader = require('../encoding/bufferreader'); var BufferWriter = require('../encoding/bufferwriter'); @@ -25,9 +24,11 @@ var BITS_64_ON = 'ffffffffffffffff'; * @param {Script} subscript the script that will be signed */ function sighash(transaction, sighashType, inputNumber, subscript) { + var Transaction = require('./transaction'); + var Input = require('./input'); + var i; // Copy transaction - var Transaction = require('./transaction'); var txcopy = Transaction.shallowCopy(transaction); // Copy script @@ -88,12 +89,14 @@ function sighash(transaction, sighashType, inputNumber, subscript) { function sign(transaction, keypair, nhashtype, nin, subscript) { var hashbuf = sighash(transaction, nhashtype, nin, subscript); + hashbuf = new BufferReader(hashbuf).readReverse(); var sig = ECDSA.sign(hashbuf, keypair, 'little').set({nhashtype: nhashtype}); return sig; } function verify(transaction, sig, pubkey, nin, subscript) { var hashbuf = sighash(transaction, sig.nhashtype, nin, subscript); + hashbuf = new BufferReader(hashbuf).readReverse(); return ECDSA.verify(hashbuf, sig, pubkey, 'little'); } diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 36ed537..8839ab7 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -10,14 +10,13 @@ var JSUtil = require('../util/js'); var BufferReader = require('../encoding/bufferreader'); var BufferWriter = require('../encoding/bufferwriter'); var Hash = require('../crypto/hash'); -var Sighash = require('./sighash'); var Signature = require('../crypto/signature'); -var errors = require('../errors'); - var Address = require('../address'); var Unit = require('../unit'); var Input = require('./input'); +var PublicKeyHashInput = Input.PublicKeyHash; +var MultiSigScriptHashInput = Input.MultiSigScriptHash; var Output = require('./output'); var Script = require('../script'); var PrivateKey = require('../privatekey'); @@ -26,16 +25,20 @@ var CURRENT_VERSION = 1; var DEFAULT_NLOCKTIME = 0; var DEFAULT_SEQNUMBER = 0xFFFFFFFF; +/** + * Represents a transaction, a set of inputs and outputs to change + * ownership of tokens + * + * @param {*} serialized + */ function Transaction(serialized) { if (!(this instanceof Transaction)) { return new Transaction(serialized); } this.inputs = []; this.outputs = []; - this._outpoints = []; this._inputAmount = 0; this._outputAmount = 0; - this._signatures = {}; if (serialized) { if (serialized instanceof Transaction) { @@ -54,6 +57,13 @@ function Transaction(serialized) { /* Constructors and Serialization */ +/** + * Create a "shallow" copy of the transaction, by serializing and deserializing + * it dropping any additional information that inputs and outputs may have hold + * + * @param {Transaction} transaction + * @return {Transaction} + */ Transaction.shallowCopy = function(transaction) { var copy = new Transaction(transaction.toBuffer()); return copy; @@ -69,10 +79,20 @@ var hashProperty = { Object.defineProperty(Transaction.prototype, 'hash', hashProperty); Object.defineProperty(Transaction.prototype, 'id', hashProperty); +/** + * Retrieve the little endian hash of the transaction (used for serialization) + * @return {Buffer} + */ Transaction.prototype._getHash = function() { return Hash.sha256sha256(this.toBuffer()); }; +/** + * Retrieve a hexa string that can be used with bitcoind's CLI interface + * (decoderawtransaction, sendrawtransaction) + * + * @return {string} + */ Transaction.prototype.serialize = Transaction.prototype.toString = function() { return this.toBuffer().toString('hex'); }; @@ -113,7 +133,6 @@ Transaction.prototype.fromBufferReader = function(reader) { for (i = 0; i < sizeTxIns; i++) { var input = Input.fromBufferReader(reader); this.inputs.push(input); - this._outpoints.push(Transaction._makeOutpoint(input)); } sizeTxOuts = reader.readVarintNum(); for (i = 0; i < sizeTxOuts; i++) { @@ -185,7 +204,13 @@ Transaction.prototype.from = function(utxo, pubkeys, threshold) { }; Transaction.prototype._fromMultiSigP2SH = function(utxo, pubkeys, threshold) { - throw new errors.NotImplemented('Transaction#_fromMultiSigP2SH'); + if (Transaction._isNewUtxo(utxo)) { + this._fromMultisigNewUtxo(utxo, pubkeys, threshold); + } else if (Transaction._isOldUtxo(utxo)) { + this._fromMultisigOldUtxo(utxo, pubkeys, threshold); + } else { + throw new Transaction.Errors.UnrecognizedUtxoFormat(utxo); + } }; Transaction.prototype._fromNonP2SH = function(utxo) { @@ -205,6 +230,11 @@ Transaction.prototype._fromNonP2SH = function(utxo) { } }; +Transaction._isNewUtxo = function(utxo) { + var isDefined = function(param) { return !_.isUndefined(param); }; + return _.all(_.map([utxo.txId, utxo.outputIndex, utxo.satoshis, utxo.script], isDefined)); +}; + Transaction._isOldUtxo = function(utxo) { var isDefined = function(param) { return !_.isUndefined(param); }; return _.all(_.map([utxo.txid, utxo.vout, utxo.scriptPubKey, utxo.amount], isDefined)); @@ -215,20 +245,15 @@ Transaction.prototype._fromOldUtxo = function(utxo) { address: utxo.address && new Address(utxo.address), txId: utxo.txid, outputIndex: utxo.vout, - script: new buffer.Buffer(utxo.scriptPubKey, 'hex'), + script: util.isHexa(utxo.script) ? new buffer.Buffer(utxo.scriptPubKey, 'hex') : utxo.scriptPubKey, satoshis: Unit.fromBTC(utxo.amount).satoshis }); }; -Transaction._isNewUtxo = function(utxo) { - var isDefined = function(param) { return !_.isUndefined(param); }; - return _.all(_.map([utxo.txId, utxo.outputIndex, utxo.satoshis, utxo.script], isDefined)); -}; - Transaction.prototype._fromNewUtxo = function(utxo) { utxo.address = utxo.address && new Address(utxo.address); utxo.script = new Script(util.isHexa(utxo.script) ? new buffer.Buffer(utxo.script, 'hex') : utxo.script); - this.inputs.push(new Input({ + this.inputs.push(new PublicKeyHashInput({ output: new Output({ script: utxo.script, satoshis: utxo.satoshis @@ -241,15 +266,30 @@ Transaction.prototype._fromNewUtxo = function(utxo) { this._inputAmount += utxo.satoshis; }; -Transaction._makeOutpoint = function(data) { - if (!_.isUndefined(data.txId) && !_.isUndefined(data.outputIndex)) { - return data.txId + ':' + data.outputIndex; - } - if (!_.isUndefined(data.prevTxId) && !_.isUndefined(data.outputIndex)) { - var prevTxId = _.isString(data.prevTxId) ? data.prevTxId : data.prevTxId.toString('hex'); - return prevTxId + ':' + data.outputIndex; - } - throw new Transaction.Errors.InvalidOutpointInfo(data); +Transaction.prototype._fromMultisigOldUtxo = function(utxo, pubkeys, threshold) { + return this._fromMultisigNewUtxo({ + address: utxo.address && new Address(utxo.address), + txId: utxo.txid, + outputIndex: utxo.vout, + script: new buffer.Buffer(utxo.scriptPubKey, 'hex'), + satoshis: Unit.fromBTC(utxo.amount).satoshis + }, pubkeys, threshold); +}; + +Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) { + utxo.address = utxo.address && new Address(utxo.address); + utxo.script = new Script(util.isHexa(utxo.script) ? new buffer.Buffer(utxo.script, 'hex') : utxo.script); + this.inputs.push(new MultiSigScriptHashInput({ + output: new Output({ + script: utxo.script, + satoshis: utxo.satoshis + }), + prevTxId: utxo.txId, + outputIndex: utxo.outputIndex, + sequenceNumber: DEFAULT_SEQNUMBER, + script: Script.empty() + }, pubkeys, threshold)); + this._inputAmount += utxo.satoshis; }; Transaction.prototype.hasAllUtxoInfo = function() { @@ -270,29 +310,12 @@ Transaction.prototype.change = function(address) { return this; }; -Transaction.prototype.to = function() { - // TODO: Type validation - var argSize = _.size(arguments); - if (argSize === 3) { - Transaction.prototype._payToMultisig.apply(this, arguments); - } else if (argSize === 2) { - Transaction.prototype._payToAddress.apply(this, arguments); - } else { - // TODO: Error - throw new Error(''); - } - return this; -}; - -Transaction.prototype._payToMultisig = function(pubkeys, threshold, amount) { - throw new errors.NotImplemented('Transaction#_payToMultisig'); -}; - -Transaction.prototype._payToAddress = function(address, amount) { +Transaction.prototype.to = function(address, amount) { this._addOutput(new Output({ - script: Script.buildPublicKeyHashOut(address), + script: Script(new Address(address)), satoshis: amount })); + return this; }; Transaction.prototype._addOutput = function(output) { @@ -310,7 +333,7 @@ Transaction.prototype.addData = function(value) { /* Signature handling */ -Transaction.prototype.sign = function(privKey) { +Transaction.prototype.sign = function(privKey, sigtype) { // TODO: Change for preconditions assert(this.hasAllUtxoInfo()); var self = this; @@ -320,7 +343,7 @@ Transaction.prototype.sign = function(privKey) { }); return this; } - _.each(this.getSignatures(privKey), function(signature) { + _.each(this.getSignatures(privKey, sigtype), function(signature) { self.applySignature(signature); }); return this; @@ -341,12 +364,23 @@ Transaction.prototype._getPrivateKeySignatures = function(privKey, sigtype) { }; Transaction.prototype.applySignature = function(signature) { - this.inputs[signature.inputIndex].addSignature(signature); + this.inputs[signature.inputIndex].addSignature(this, signature); return this; }; -Transaction.prototype.getSignatures = function(privKey) { - return this._getPrivateKeySignatures(privKey); +Transaction.prototype.getSignatures = function(privKey, sigtype) { + return this._getPrivateKeySignatures(privKey, sigtype); +}; + +Transaction.prototype.isFullySigned = function() { + return _.all(_.map(this.inputs, function(input) { + return input.isFullySigned(); + })); +}; + +Transaction.prototype.isValidSignature = function(signature) { + var self = this; + return this.inputs[signature.inputIndex].isValidSignature(self, signature); }; module.exports = Transaction; diff --git a/lib/util/js.js b/lib/util/js.js index 689b0ac..a481987 100644 --- a/lib/util/js.js +++ b/lib/util/js.js @@ -33,6 +33,13 @@ module.exports = { isHexa: isHexa, isHexaString: isHexa, + /** + * Clone an array + */ + cloneArray: function(array) { + return [].concat(array); + }, + /** * Define immutable properties on a target object * diff --git a/test/transaction.js b/test/transaction.js deleted file mode 100644 index 06187cf..0000000 --- a/test/transaction.js +++ /dev/null @@ -1,139 +0,0 @@ -'use strict'; - -/* jshint unused: false */ -/* jshint latedef: false */ -var should = require('chai').should(); -var _ = require('lodash'); - -var bitcore = require('..'); -var Transaction = bitcore.Transaction; -var Script = bitcore.Script; - -var valid = require('./data/bitcoind/tx_valid.json'); -var invalid = require('./data/bitcoind/tx_invalid.json'); - -describe('Transaction', function() { - - describe('bitcoind compliance', function() { - - valid.map(function(datum){ - if ( typeof(datum[0]) === 'string' ) { - return; - } - - it('should deserialize/serialize '+datum[1].slice(0, 15)+'... transaction', function() { - var serialized = datum[1]; - var t = new Transaction(serialized); - t.serialize().should.equal(serialized); - }); - - }); - - }); - - it('should serialize and deserialize correctly a given transaction', function() { - var transaction = new Transaction(tx_1_hex); - transaction.serialize().should.equal(tx_1_hex); - }); - - it('should display correctly in console', function() { - var transaction = new Transaction(tx_1_hex); - transaction.inspect().should.equal(''); - }); - - it('standard hash of transaction should be decoded correctly', function() { - var transaction = new Transaction(tx_1_hex); - transaction.id.should.equal(tx_1_id); - }); - - it('serializes an empty transaction', function() { - var transaction = new Transaction(); - transaction.serialize().should.equal(tx_empty_hex); - }); - - it('serializes and deserializes correctly', function() { - var transaction = new Transaction(tx_1_hex); - transaction.serialize().should.equal(tx_1_hex); - }); - - it('should input/output json', function() { - var transaction = JSON.parse(Transaction().fromJSON(tx_1_json).toJSON()); - transaction.should.deep.equal(JSON.parse(tx_1_json)); - }); - - it('should create a sample transaction from an utxo', function() { - var transaction = new Transaction() - .from(utxo_1a) - .to(address_1, amount_1) - .sign(privkey_1a) - .serialize() - .should.equal(tx_1_hex); - }); - - it.skip('should create a transaction with two utxos', function() { - var transaction = new Transaction() - .from([utxo_2a, utxo_2b]) - .to(address_2, amount_2) - .sign([privkey_2a, privkey_2b]) - .serialize() - .should.equal(tx_2_hex); - }); -}); - -var tx_empty_hex = '01000000000000000000'; - -/* jshint maxlen: 1000 */ -var tx_1_hex = '01000000015884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4000000006a473044022013fa3089327b50263029265572ae1b022a91d10ac80eb4f32f291c914533670b02200d8a5ed5f62634a7e1a0dc9188a3cc460a986267ae4d58faf50c79105431327501210223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5effffffff0150690f00000000001976a9147821c0a3768aa9d1a37e16cf76002aef5373f1a888ac00000000'; -var tx_1_id = '779a3e5b3c2c452c85333d8521f804c1a52800e60f4b7c3bbe36f4bab350b72c'; -var tx_2_hex = ''; - -var tx_1_json = JSON.stringify({ - version:1, - inputs:[{ - prevTxId:"a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458", - outputIndex:0, - sequenceNumber:4294967295, - script:'71 0x3044022013fa3089327b50263029265572ae1b022a91d10ac80eb4f32f291c914533670b02200d8a5ed5f62634a7e1a0dc9188a3cc460a986267ae4d58faf50c79105431327501 33 0x0223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5e'}], - outputs:[{ - satoshis:1010000, - script:'OP_DUP OP_HASH160 20 0x7821c0a3768aa9d1a37e16cf76002aef5373f1a8 OP_EQUALVERIFY OP_CHECKSIG' - }], - nLockTime:0 -}); - -var utxo_1a_address = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1'; - -var utxo_2a_address = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc'; -var utxo_2b_address = 'mrCHmWgn54hJNty2srFF4XLmkey5GnCv5m'; - -/* A new-format utxo */ -var utxo_1a = { - address: utxo_1a_address, - txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', - outputIndex: 0, - script: Script.buildPublicKeyHashOut(utxo_1a_address).toString(), - satoshis: 1020000 -}; -/* An old-format utxo */ -var utxo_2a = { - address: utxo_2a_address, - txid: '779a3e5b3c2c452c85333d8521f804c1a52800e60f4b7c3bbe36f4bab350b72c', - vout: 0, - scriptPubKey: Script.buildPublicKeyHashOut(utxo_2a_address).toString(), - amount: 0.01010000 -}; -var utxo_2b = { - address: utxo_2b_address, - txid: 'e0f44096fcac31c1baede0714997c831123ecb5e258b52617fb093ba487c1d04', - vout: 0, - scriptPubKey: Script.buildPublicKeyHashOut(utxo_2b_address).toString(), - amount: 0.00090000 -}; - -var address_1 = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc'; -var address_2 = 'mrCHmWgn54hJNty2srFF4XLmkey5GnCv5m'; -var amount_1 = 1010000; -var amount_2 = 1090000; -var privkey_1a = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY'; -var privkey_2a = 'cVLKm6LT1VTpZJVaSYtkYPLP1UP2Ph6NFxGVNLPAKKuSfv8hHreU'; -var privkey_2b = 'cVWHj19aJXVAxcKC5xAWQmiyhWyarmcPcuv4dT7nZy1JR37dbWgT'; diff --git a/test/transaction/creation.js b/test/transaction/creation.js new file mode 100644 index 0000000..33657c6 --- /dev/null +++ b/test/transaction/creation.js @@ -0,0 +1,79 @@ +'use strict'; + +var bitcore = require('../..'); +var Script = bitcore.Script; + +module.exports = [ + [ + 'from', [{ + address: 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1', + txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', + outputIndex: 0, + script: Script.buildPublicKeyHashOut('mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1').toString(), + satoshis: 1020000 + }], + 'to', ['mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', 1010000], + 'sign', ['cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY'], + 'serialize', '01000000015884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4000000006b4830450221009972100061da4a17a471ac1906c18bb5445c03da2a0be52c59aca6c58f1e342302205eac5ba43830a397f613f40addea4a2eeaa485a1f9a6efa61344c3560762fe3d01210223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5effffffff0150690f00000000001976a9147821c0a3768aa9d1a37e16cf76002aef5373f1a888ac00000000' + ], + [ + 'from', [{ + "txid" : "e42447187db5a29d6db161661e4bc66d61c3e499690fe5ea47f87b79ca573986", + "vout" : 1, + "address" : "mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up", + "scriptPubKey" : "76a914073b7eae2823efa349e3b9155b8a735526463a0f88ac", + "amount" : 0.01080000, + }], + 'to', ['mn9new5vPYWuVN5m3gUBujfKh1uPQvR9mf', 500000], + 'to', ['mw5ctwgEaNRbxkM4JhXH3rp5AyGvTWDZCD', 570000], + 'sign', ['cSQUuwwJBAg6tYQhzqqLWW115D1s5KFZDyhCF2ffrnukZxMK6rNZ'], + 'serialize', '0100000001863957ca797bf847eae50f6999e4c3616dc64b1e6661b16d9da2b57d184724e4010000006b483045022100855691c90510edf83ab632f0a0b17f5202d2cf7071050dcf0c2778325ed403cd02207270a2f0b30c13dc3c1dee74b5ccabcc2632b402c4f38adabcd07357df1442270121039dd446bbc85db6917f39c0b4c295b0f8cce76d1926fa76d7b84e3f7ff1c5eec5ffffffff0220a10700000000001976a91448c819246ae5645ceecd41fbe1aa6202a0a9b5ca88ac90b20800000000001976a914aab76ba4877d696590d94ea3e02948b55294815188ac00000000' + ], + [ + 'from', [[{ + "txid" : "a9db84566e0fc9351e86337d2828ab281b25ddc06fab798f6d4b5baef48c02b3", + "vout" : 0, + "address" : "mn9new5vPYWuVN5m3gUBujfKh1uPQvR9mf", + "account" : "", + "scriptPubKey" : "76a91448c819246ae5645ceecd41fbe1aa6202a0a9b5ca88ac", + "amount" : 0.00500000, + "confirmations" : 0 + }, { + "txid" : "a9db84566e0fc9351e86337d2828ab281b25ddc06fab798f6d4b5baef48c02b3", + "vout" : 1, + "address" : "mw5ctwgEaNRbxkM4JhXH3rp5AyGvTWDZCD", + "account" : "", + "scriptPubKey" : "76a914aab76ba4877d696590d94ea3e02948b55294815188ac", + "amount" : 0.00570000, + "confirmations" : 0 + }]], + 'to', ['mtymcCX5KixPjT1zxtg59qewBGWptj9etH', 1060000], + 'sign', [['cPGbA2C54ZZ1sw4dc2ckBE1WqkdrNSbEV8Tkjhi2p1J15oErdgP2', 'cSpyve5bXAuyHrNeV9MjTdFz3HLw739yUjjUAUSMe3ppf2qzj2hw']], + 'serialize', '0100000002b3028cf4ae5b4b6d8f79ab6fc0dd251b28ab28287d33861e35c90f6e5684dba9000000006a4730440220635e95e1981bbb360feaf4c232f626a0af8eb5c043a99749a21b0e37fd0048fd02207889f6974f0cad39ce8c2a6dff05c8ca402da9ff6fc41e06c12d86853c91a9d80121030253c73236acf5ea9085d408220141197f6094de07426bd0d32c7a543614fdd7ffffffffb3028cf4ae5b4b6d8f79ab6fc0dd251b28ab28287d33861e35c90f6e5684dba9010000006a4730440220319a0b5ee9c67ccb7de4222234f31059354be4f239c99ca24bff30adfec8e8ec022056e6e99e50f7ceaa062958b8424cde1d504019f95c1dc0a0f0778848d0fb9f4b012102977a001a0a7bbfd1f8a647c7d46e13e8f6920635b328390b43b3303977101149ffffffff01a02c1000000000001976a91493abf1e9e4a20c125b93f93ee39efc16b6e4bc4688ac00000000' + ], + [ + 'from', [{ + "txid": "c8beceb964dec7ae5ec6ef5d019429b50c2e5fd07bd369e9a282d5153f23589c", + "vout": 0, + "address": "mtymcCX5KixPjT1zxtg59qewBGWptj9etH", + "account": "", + "scriptPubKey": "76a91493abf1e9e4a20c125b93f93ee39efc16b6e4bc4688ac", + "amount": 0.01060000, + }], + 'to', ['2NEQb8rtiUgxqQ9eif4XVeMUEW2LSZ64s58', 1050000], + 'sign', ['cMh7xdJ5EZVg6kvFsBybwK1EYGJw3G1DHhe5sNPAwbDts94ohKyK'], + 'serialize', '01000000019c58233f15d582a2e969d37bd05f2e0cb52994015defc65eaec7de64b9cebec8000000006a473044022050442862e892b1d12bcaa03857746f0ed168122e093d799861f4e081756bb8aa0220081d4eaf9281ae8f954efaeb47500d9a02e5a74b3ada51b6a258ac83c1f4f6420121039dbeac2610d53eb7107b14c0fa9be4006a731fa5bcef392d4e1a25ec0e58f0d3ffffffff01900510000000000017a91490edc43da6b052c4a23fc178979ce358a8caad5e8700000000' + ], + [ + 'from', [{ + "address": "2N6TY8Dc5JmJ87Fg9DhmN66fvFSwnTrjgip", + "txid": "66e64ef8a3b384164b78453fa8c8194de9a473ba14f89485a0e433699daec140", + "vout": 0, + "scriptPubKey": "a91490edc43da6b052c4a23fc178979ce358a8caad5e87", + "amount": 0.01050000 + }, ['03fd45c8cd28c4c6a9a89b4515173edebc66a2418353976eccf01c73a7da9bbb12', '0349e0138b2c2f496121258e0426e1dbd698b2c6038e70fd17e3563aa87b4384f9'], 2], + 'to', ['mssMdcEm6PiJEr4XZtjk6kkai84EjBbi91', 1040000], + 'sign', [['L3wRFe9XHLnkLquf41F56ac77uRXwJ97HZPQ9tppqyMANBKXpoc5', 'KzkfNSL1gvdyU3CGLaP1Cs3pW167M8r9uE8yMtWQrAzz5vCv59CM']], + 'serialize', '010000000140c1ae9d6933e4a08594f814ba73a4e94d19c8a83f45784b1684b3a3f84ee66600000000da004730440220366678972728684a94f35635b855583603b28065d430949c08be89412a4ee45d02201aa62e3129c8819ecf2048230e8c77e244d6a496f296954a5bb4a0d0185f8c0201483045022100d06f348b4ef793f2bf749b288f1df165c0946779391c50ddc050e5b1608b2dda02200fcc8c6874b9a313374020253c5de346fe3517c97b18bfa769cea1089ad97144014752210349e0138b2c2f496121258e0426e1dbd698b2c6038e70fd17e3563aa87b4384f92103fd45c8cd28c4c6a9a89b4515173edebc66a2418353976eccf01c73a7da9bbb1252aeffffffff0180de0f00000000001976a914877d4f3be444448f868b345153bc4fc7a11a7c6388ac00000000' + ] +]; diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js new file mode 100644 index 0000000..c7612e8 --- /dev/null +++ b/test/transaction/transaction.js @@ -0,0 +1,70 @@ +'use strict'; + +/* jshint unused: false */ +/* jshint latedef: false */ +var should = require('chai').should(); +var _ = require('lodash'); + +var bitcore = require('../..'); +var Transaction = bitcore.Transaction; +var PrivateKey = bitcore.PrivateKey; +var Script = bitcore.Script; +var Address = bitcore.Address; +var Networks = bitcore.Networks; + +var transactionVector = require('./creation'); + +describe('Transaction', function() { + + it('should serialize and deserialize correctly a given transaction', function() { + var transaction = new Transaction(tx_1_hex); + transaction.serialize().should.equal(tx_1_hex); + }); + + it('should display correctly in console', function() { + var transaction = new Transaction(tx_1_hex); + transaction.inspect().should.equal(''); + }); + + it('standard hash of transaction should be decoded correctly', function() { + var transaction = new Transaction(tx_1_hex); + transaction.id.should.equal(tx_1_id); + }); + + it('serializes an empty transaction', function() { + var transaction = new Transaction(); + transaction.serialize().should.equal(tx_empty_hex); + }); + + it('serializes and deserializes correctly', function() { + var transaction = new Transaction(tx_1_hex); + transaction.serialize().should.equal(tx_1_hex); + }); + + describe('transaction creation test vector', function() { + var index = 0; + transactionVector.forEach(function(vector) { + index++; + it('case ' + index, function() { + var i = 0; + var transaction = new Transaction(); + while (i < vector.length) { + var command = vector[i]; + var args = vector[i+1]; + if (command === 'serialize') { + transaction.serialize().should.equal(args); + } else { + transaction[command].apply(transaction, args); + } + i += 2; + } + }); + }); + }); +}); + +var tx_empty_hex = '01000000000000000000'; + +/* jshint maxlen: 1000 */ +var tx_1_hex = '01000000015884e5db9de218238671572340b207ee85b628074e7e467096c267266baf77a4000000006a473044022013fa3089327b50263029265572ae1b022a91d10ac80eb4f32f291c914533670b02200d8a5ed5f62634a7e1a0dc9188a3cc460a986267ae4d58faf50c79105431327501210223078d2942df62c45621d209fab84ea9a7a23346201b7727b9b45a29c4e76f5effffffff0150690f00000000001976a9147821c0a3768aa9d1a37e16cf76002aef5373f1a888ac00000000'; +var tx_1_id = '779a3e5b3c2c452c85333d8521f804c1a52800e60f4b7c3bbe36f4bab350b72c';