diff --git a/lib/errors/spec.js b/lib/errors/spec.js index 8b62187..2a813e4 100644 --- a/lib/errors/spec.js +++ b/lib/errors/spec.js @@ -34,6 +34,28 @@ module.exports = [{ }, { name: 'InvalidArgumentType', message: format('Invalid Argument for {2}, expected {1} but got ') + '+ typeof arguments[0]', + }, { + name: 'Transaction', + message: format('Internal Error on Transaction {0}'), + errors: [ + { + name: 'Input', + message: format('Internal Error on Input {0}'), + errors: [{ + name: 'MissingScript', + message: format('Need a script to create an input') + }] + }, { + name: 'NeedMoreInfo', + message: format('{0}') + }, { + name: 'FeeError', + message: format('Fees are not correctly set {0}'), + }, { + name: 'ChangeAddressMissing', + message: format('Change address is missing') + } + ] }, { name: 'Script', message: format('Internal Error on Script {0}'), diff --git a/lib/transaction/input/input.js b/lib/transaction/input/input.js index 0d90982..73041f7 100644 --- a/lib/transaction/input/input.js +++ b/lib/transaction/input/input.js @@ -8,6 +8,7 @@ var BufferUtil = require('../../util/buffer'); var JSUtil = require('../../util/js'); var Script = require('../../script'); var Sighash = require('../sighash'); +var Output = require('../output'); function Input(params) { if (!(this instanceof Input)) { @@ -33,12 +34,15 @@ Input.prototype._fromObject = function(params) { if (_.isString(params.prevTxId) && JSUtil.isHexa(params.prevTxId)) { params.prevTxId = new buffer.Buffer(params.prevTxId, 'hex'); } - this.output = params.output; + this.output = params.output ? + (params.output instanceof Output ? params.output : new Output(params.output)) : undefined; this.prevTxId = params.prevTxId; this.outputIndex = params.outputIndex; this.sequenceNumber = params.sequenceNumber; - if (params.script || params.scriptBuffer) { - this.setScript(params.script || params.scriptBuffer); + if (!_.isUndefined(params.script) || !_.isUndefined(params.scriptBuffer)) { + this.setScript(_.isUndefined(params.script) ? params.scriptBuffer : params.script); + } else { + throw new errors.Transaction.Input.MissingScript(); } return this; }; @@ -48,7 +52,8 @@ Input.prototype.toObject = function toObject() { prevTxId: this.prevTxId.toString('hex'), outputIndex: this.outputIndex, sequenceNumber: this.sequenceNumber, - script: this._script.toString() + script: this.script.toString(), + output: this.output ? this.output.toObject() : undefined }; }; @@ -61,6 +66,7 @@ Input.fromJSON = function(json) { json = JSON.parse(json); } return new Input({ + output: json.output ? new Output(json.output) : undefined, prevTxId: json.prevTxId || json.txidbuf, outputIndex: _.isUndefined(json.outputIndex) ? json.txoutnum : json.outputIndex, sequenceNumber: json.sequenceNumber || json.seqnum, @@ -99,11 +105,13 @@ Input.prototype.setScript = function(script) { if (script instanceof Script) { this._script = script; this._scriptBuffer = script.toBuffer(); + } else if (_.isString(script)) { + this._script = new Script(script); + this._scriptBuffer = this._script.toBuffer(); } else if (BufferUtil.isBuffer(script)) { this._script = null; this._scriptBuffer = new buffer.Buffer(script); } else { - console.log(script); throw new TypeError('Invalid Argument'); } return this; @@ -156,4 +164,10 @@ Input.prototype.isNull = function() { this.outputIndex === 0xffffffff; }; +Input.prototype._estimateSize = function() { + var bufferWriter = new BufferWriter(); + this.toBufferWriter(bufferWriter); + return bufferWriter.toBuffer().length; +}; + module.exports = Input; diff --git a/lib/transaction/input/multisigscripthash.js b/lib/transaction/input/multisigscripthash.js index 21646fd..1a36a14 100644 --- a/lib/transaction/input/multisigscripthash.js +++ b/lib/transaction/input/multisigscripthash.js @@ -1,7 +1,6 @@ 'use strict'; var _ = require('lodash'); -var JSUtil = require('../../util/js'); var inherits = require('inherits'); var Input = require('./input'); var Output = require('../output'); @@ -123,11 +122,14 @@ MultiSigScriptHashInput.prototype.isValidSignature = function(transaction, signa ); }; -MultiSigScriptHashInput.OPCODES_SIZE = 10; -MultiSigScriptHashInput.SIGNATURE_SIZE = 36; +MultiSigScriptHashInput.OPCODES_SIZE = 7; // serialized size (<=3) + 0 .. N .. M OP_CHECKMULTISIG +MultiSigScriptHashInput.SIGNATURE_SIZE = 74; // size (1) + DER (<=72) + sighash (1) +MultiSigScriptHashInput.PUBKEY_SIZE = 34; // size (1) + DER (<=33) MultiSigScriptHashInput.prototype._estimateSize = function() { - return MultiSigScriptHashInput.OPCODES_SIZE + this.threshold * MultiSigScriptHashInput.SIGNATURE_SIZE; + return MultiSigScriptHashInput.OPCODES_SIZE + + this.threshold * MultiSigScriptHashInput.SIGNATURE_SIZE + + this.publicKeys.length * MultiSigScriptHashInput.PUBKEY_SIZE; }; module.exports = MultiSigScriptHashInput; diff --git a/lib/transaction/input/publickeyhash.js b/lib/transaction/input/publickeyhash.js index cdd8c06..c66ec26 100644 --- a/lib/transaction/input/publickeyhash.js +++ b/lib/transaction/input/publickeyhash.js @@ -85,10 +85,10 @@ PublicKeyHashInput.prototype.isFullySigned = function() { return this.script.isPublicKeyHashIn(); }; -PublicKeyHashInput.FIXED_SIZE = 32 + 4 + 2; -PublicKeyHashInput.SCRIPT_MAX_SIZE = 34 + 20; +PublicKeyHashInput.SCRIPT_MAX_SIZE = 73 + 34; // sigsize (1 + 72) + pubkey (1 + 33) + PublicKeyHashInput.prototype._estimateSize = function() { - return PublicKeyHashInput.FIXED_SIZE + PublicKeyHashInput.SCRIPT_MAX_SIZE; + return PublicKeyHashInput.SCRIPT_MAX_SIZE; }; module.exports = PublicKeyHashInput; diff --git a/lib/transaction/output.js b/lib/transaction/output.js index c7a988d..e1a89c3 100644 --- a/lib/transaction/output.js +++ b/lib/transaction/output.js @@ -54,7 +54,7 @@ Output.prototype._fromObject = function(param) { Output.prototype.toObject = function toObject() { return { satoshis: this.satoshis, - script: this._script.toString() + script: this.script.toString() }; }; @@ -76,10 +76,14 @@ Output.prototype.setScript = function(script) { if (script instanceof Script) { this._scriptBuffer = script.toBuffer(); this._script = script; + } else if (_.isString(script)) { + this._script = new Script(script); + this._scriptBuffer = this._script.toBuffer(); } else if (bufferUtil.isBuffer(script)) { this._scriptBuffer = script; this._script = null; } else { + console.log(script); throw new TypeError('Unrecognized Argument'); } return this; diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 74bf16a..588b685 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -3,8 +3,8 @@ var _ = require('lodash'); var $ = require('../util/preconditions'); var buffer = require('buffer'); -var assert = require('assert'); +var errors = require('../errors'); var util = require('../util/js'); var bufferUtil = require('../util/buffer'); var JSUtil = require('../util/js'); @@ -53,6 +53,8 @@ function Transaction(serialized) { this.fromBuffer(serialized); } else if (_.isObject(serialized)) { this.fromObject(serialized); + } else { + throw new errors.InvalidArgument('Must provide an object or string to deserialize a transaction'); } } else { this._newTransaction(); @@ -98,12 +100,48 @@ Transaction.prototype._getHash = function() { * Retrieve a hexa string that can be used with bitcoind's CLI interface * (decoderawtransaction, sendrawtransaction) * + * @param {boolean=} unsafe if true, skip testing for fees that are too high * @return {string} */ -Transaction.prototype.serialize = Transaction.prototype.toString = function() { +Transaction.prototype.serialize = function(unsafe) { + if (unsafe) { + return this.uncheckedSerialize(); + } else { + return this.checkedSerialize(); + } +}; + +Transaction.prototype.uncheckedSerialize = Transaction.prototype.toString = function() { return this.toBuffer().toString('hex'); }; +Transaction.prototype.checkedSerialize = Transaction.prototype.toString = function() { + var feeError = this._validateFees(); + if (feeError) { + var changeError = this._validateChange(); + if (changeError) { + throw new errors.Transaction.ChangeAddressMissing(); + } else { + throw new errors.Transaction.FeeError(feeError); + } + } + return this.uncheckedSerialize(); +}; + +Transaction.FEE_SECURITY_MARGIN = 15; + +Transaction.prototype._validateFees = function() { + if (this._getUnspentValue() > Transaction.FEE_SECURITY_MARGIN * this._estimateFee()) { + return 'Fee is more than ' + Transaction.FEE_SECURITY_MARGIN + ' times the suggested amount'; + } +}; + +Transaction.prototype._validateChange = function() { + if (!this._change) { + return 'Missing change address'; + } +}; + Transaction.prototype.inspect = function() { return ''; }; @@ -186,6 +224,18 @@ Transaction.prototype.toObject = function toObject() { }; }; +Transaction.prototype.fromObject = function(transaction) { + var self = this; + _.each(transaction.inputs, function(input) { + self.addInput(new Input(input)); + }); + _.each(transaction.outputs, function(output) { + self.addOutput(new Output(output)); + }); + this.nLockTime = transaction.nLockTime; + this.version = transaction.version; +}; + Transaction.prototype.toJSON = function toJSON() { return JSON.stringify(this.toObject()); }; @@ -329,10 +379,9 @@ Transaction.prototype._fromMultisigOldUtxo = function(utxo, pubkeys, threshold) }; Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) { - this._changeSetup = false; 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({ + this.addInput(new MultiSigScriptHashInput({ output: new Output({ script: utxo.script, satoshis: utxo.satoshis @@ -342,7 +391,49 @@ Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) sequenceNumber: DEFAULT_SEQNUMBER, script: Script.empty() }, pubkeys, threshold)); - this._inputAmount += utxo.satoshis; +}; + +/** + * Add an input to this transaction. The input must be an instance of the `Input` class. + * It should have information about the Output that it's spending, but if it's not already + * set, two additional parameters, `outputScript` and `satoshis` can be provided. + * + * @param {Input} input + * @param {String|Script} outputScript + * @param {number} satoshis + * @return Transaction this, for chaining + */ +Transaction.prototype.addInput = function(input, outputScript, satoshis) { + $.checkArgumentType(input, Input, 'input'); + if (!input.output || !(input.output instanceof Output) && !outputScript && !satoshis) { + throw new errors.Transaction.NeedMoreInfo('Need information about the UTXO script and satoshis'); + } + if (!input.output && outputScript && satoshis) { + outputScript = outputScript instanceof Script ? outputScript : new Script(outputScript); + $.checkArgumentType(satoshis, 'number', 'satoshis'); + input.output = new Output({ + script: outputScript, + satoshis: satoshis + }); + } + return this.uncheckedAddInput(input); +}; + +/** + * Add an input to this transaction, without checking that the input has information about + * the output that it's spending. + * + * @param {Input} input + * @return Transaction this, for chaining + */ +Transaction.prototype.uncheckedAddInput = function(input) { + $.checkArgumentType(input, Input, 'input'); + this._changeSetup = false; + this.inputs.push(input); + if (input.output) { + this._inputAmount += input.output.satoshis; + } + return this; }; /** @@ -398,7 +489,7 @@ Transaction.prototype.change = function(address) { * @return {Transaction} this, for chaining */ Transaction.prototype.to = function(address, amount) { - this._addOutput(new Output({ + this.addOutput(new Output({ script: Script(new Address(address)), satoshis: amount })); @@ -416,14 +507,15 @@ Transaction.prototype.to = function(address, amount) { * @return {Transaction} this, for chaining */ Transaction.prototype.addData = function(value) { - this._addOutput(new Output({ + this.addOutput(new Output({ script: Script.buildDataOut(value), satoshis: 0 })); return this; }; -Transaction.prototype._addOutput = function(output) { +Transaction.prototype.addOutput = function(output) { + $.checkArgumentType(output, Output, 'output'); this.outputs.push(output); this._changeSetup = false; this._outputAmount += output.satoshis; @@ -442,12 +534,11 @@ Transaction.prototype._updateChangeOutput = function() { if (!_.isUndefined(this._changeOutput)) { this.removeOutput(this._changeOutput); } - var estimatedSize = this._estimateSize(); - var available = this._inputAmount - this._outputAmount; - var fee = this._fee || Transaction._estimateFee(estimatedSize, available); + var available = this._getUnspentValue(); + var fee = this.getFee(); if (available - fee > 0) { this._changeOutput = this.outputs.length; - this._addOutput(new Output({ + this.addOutput(new Output({ script: Script.fromAddress(this._change), satoshis: available - fee })); @@ -457,6 +548,20 @@ Transaction.prototype._updateChangeOutput = function() { this._changeSetup = true; }; +Transaction.prototype.getFee = function() { + return this._fee || this._estimateFee(); +}; + +Transaction.prototype._estimateFee = function() { + var estimatedSize = this._estimateSize(); + var available = this._getUnspentValue(); + return Transaction._estimateFee(estimatedSize, available); +}; + +Transaction.prototype._getUnspentValue = function() { + return this._inputAmount - this._outputAmount; +}; + Transaction.prototype._clearSignatures = function() { _.each(this.inputs, function(input) { input.clearSignatures(); @@ -472,7 +577,7 @@ Transaction._estimateFee = function(size, amountAvailable) { // Safe upper bound for change address script size += Transaction.CHANGE_OUTPUT_MAX_SIZE; } - return Math.ceil(size / 1000 / Transaction.FEE_PER_KB) * 1000; + return Math.ceil(size / 1000) * Transaction.FEE_PER_KB; }; Transaction.MAXIMUM_EXTRA_SIZE = 4 + 9 + 9 + 4; diff --git a/test/data/tx_creation.json b/test/data/tx_creation.json index 151edfb..0cab72f 100644 --- a/test/data/tx_creation.json +++ b/test/data/tx_creation.json @@ -80,6 +80,6 @@ "change", ["3BazTqvkvEBcWk7J4sbgRnxUw6rjYrogf9"], "sign", ["L2U9m5My3cdyN5qX1PH4B7XstGDZFWwyukdX8gj8vsJ3fkrqArQo"], "sign", ["L4jFVcDaqZCkknP5KQWjCBgiLFxKxRxywNGTucm3jC3ozByZcbZv"], - "serialize", "010000000220c24f763536edb05ce8df2a4816d971be4f20b58451d71589db434aca98bfaf00000000fdfe0000483045022100f71c1c4d174c41c9ecd57306f46c9a04ab79f8ee69af54ab47ab17992622e675022047d850988556cf73ac7d0642be67a707ae3ba71018f9ebbf98cf8ca67da1c52f01483045022100b8360e5ad52099cb5a0d624fc73a905871a36ba43b77a93234ea64c47fc2d558022042991aecde368acefcfa8af60b49824a71d76e85331eecff1d82b3e3d7bcdbce014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffffa0644cd1606e081c59eb65fe69d4a83a3a822da423bc392c91712fb77a192edc00000000fdfd000047304402201065d3d9e7009f8c53ac802fe3757009e21a0849138acf96eab860912293b5ee022014552a7c8e2d84bf46468a7927a414eb0cf8cd42601cd7b6d07f44558afbfce901483045022100c7b3d9df174e6b97d3832a9d2cf75b064ae09b258e56448601c6f9e2ea91f0e3022005a723ed84e5b6c8c4ab6637176dd2adb8cb1cd618550cb9fd0e9a0d4db51184014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffff03f04902000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af387007102000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af38763b204000000000017a9146c8d8b04c6a1e664b1ec20ec932760760c97688e8700000000" + "serialize", "010000000220c24f763536edb05ce8df2a4816d971be4f20b58451d71589db434aca98bfaf00000000fdfd000047304402202c34fd898bbea3521c5f88fdeef4bb65f36ce01142633c1121e9b7bef307938902205e1aad62a66bd294898dc71678a4a5448281126baad90bfd9b4139663e852d26014830450221009d9f2b46595dc2578f4c4ac65c779e45e493bcc4649ef6786477e40df40d21fc02206da8a0f80fa2aa2d2b89d56951761c8f0506fdafd53ef6fe7d5c878251bb216b014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffffa0644cd1606e081c59eb65fe69d4a83a3a822da423bc392c91712fb77a192edc00000000fdfd000047304402202e64f052cd5be367f2f9efb31d3a34d0f8339700beff426d31b53c9a776a0368022057c714fda22a5ec301765dd187be3c522bcfe040127e857067163a5fb9ed46c001483045022100bf7a1e5a9e7204361e70312e15746efc95c7be8957a66c76574d2a07db359c550220318dc70b703c84a76fe3a460e96d466462f4c1c54b9fbfb75157b1afe2c51f8e014c695221020483ebb834d91d494a3b649cf0e8f5c9c4fcec5f194ab94341cc99bb440007f2210271ebaeef1c2bf0c1a4772d1391eab03e4d96a6e9b48551ab4e4b0d2983eb452b2103a659828aabe443e2dedabb1db5a22335c5ace5b5b7126998a288d63c99516dd853aeffffffff03f04902000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af387007102000000000017a9144de752833233fe69a20064f29b2ca0f6399c8af3873b8f04000000000017a9146c8d8b04c6a1e664b1ec20ec932760760c97688e8700000000" ] ] diff --git a/test/script/interpreter.js b/test/script/interpreter.js index 2b6eac5..7be035e 100644 --- a/test/script/interpreter.js +++ b/test/script/interpreter.js @@ -202,26 +202,26 @@ describe('Interpreter', function() { var hashbuf = new Buffer(32); hashbuf.fill(0); var credtx = Transaction(); - credtx.inputs.push(new Transaction.Input({ + credtx.uncheckedAddInput(new Transaction.Input({ prevTxId: '0000000000000000000000000000000000000000000000000000000000000000', outputIndex: 0xffffffff, sequenceNumber: 0xffffffff, script: Script('OP_0 OP_0') })); - credtx._addOutput(new Transaction.Output({ + credtx.addOutput(new Transaction.Output({ script: scriptPubkey, satoshis: 0 })); var idbuf = credtx.id; var spendtx = Transaction(); - spendtx.inputs.push(new Transaction.Input({ + spendtx.uncheckedAddInput(new Transaction.Input({ prevTxId: idbuf.toString('hex'), outputIndex: 0, sequenceNumber: 0xffffffff, script: scriptSig })); - spendtx._addOutput(new Transaction.Output({ + spendtx.addOutput(new Transaction.Output({ script: Script(), satoshis: 0 })); diff --git a/test/transaction/input/input.js b/test/transaction/input/input.js new file mode 100644 index 0000000..7b65129 --- /dev/null +++ b/test/transaction/input/input.js @@ -0,0 +1,46 @@ +'use strict'; + +var should = require('chai').should(); +var expect = require('chai').expect; +var _ = require('lodash'); + +var bitcore = require('../../..'); +var errors = bitcore.errors; +var PrivateKey = bitcore.PrivateKey; +var Address = bitcore.Address; +var Script = bitcore.Script; +var Networks = bitcore.Networks; +var Input = bitcore.Transaction.Input; + +describe('Transaction.Input', function() { + + var privateKey = new PrivateKey('KwF9LjRraetZuEjR8VqEq539z137LW5anYDUnVK11vM3mNMHTWb4'); + var publicKey = privateKey.publicKey; + var address = new Address(publicKey, Networks.livenet); + var output = { + address: '33zbk2aSZYdNbRsMPPt6jgy6Kq1kQreqeb', + prevTxId: '66e64ef8a3b384164b78453fa8c8194de9a473ba14f89485a0e433699daec140', + outputIndex: 0, + script: new Script(address), + satoshis: 1000000 + }; + var coinbase = { + prevTxId: '0000000000000000000000000000000000000000000000000000000000000000', + outputIndex: 0xFFFFFFFF, + script: new Script(), + satoshis: 1000000 + }; + + it('has abstract methods: "getSignatures", "isFullySigned", "addSignature", "clearSignatures"', function() { + var input = new Input(output); + _.each(['getSignatures', 'isFullySigned', 'addSignature', 'clearSignatures'], function(method) { + expect(function() { + return input[method](); + }).to.throw(errors.AbstractMethodInvoked); + }); + }); + it('detects coinbase transactions', function() { + new Input(output).isNull().should.equal(false); + new Input(coinbase).isNull().should.equal(true); + }); +}); diff --git a/test/transaction/input/multisigscripthash.js b/test/transaction/input/multisigscripthash.js new file mode 100644 index 0000000..c9a3621 --- /dev/null +++ b/test/transaction/input/multisigscripthash.js @@ -0,0 +1,96 @@ +'use strict'; +/* jshint unused: false */ + +var should = require('chai').should(); +var expect = require('chai').expect; +var _ = require('lodash'); + +var bitcore = require('../../..'); +var Transaction = bitcore.Transaction; +var PrivateKey = bitcore.PrivateKey; +var Address = bitcore.Address; +var Script = bitcore.Script; +var Signature = bitcore.crypto.Signature; + +describe('MultiSigScriptHashInput', function() { + + var privateKey1 = new PrivateKey('KwF9LjRraetZuEjR8VqEq539z137LW5anYDUnVK11vM3mNMHTWb4'); + var privateKey2 = new PrivateKey('L4PqnaPTCkYhAqH3YQmefjxQP6zRcF4EJbdGqR8v6adtG9XSsadY'); + var privateKey3 = new PrivateKey('L4CTX79zFeksZTyyoFuPQAySfmP7fL3R41gWKTuepuN7hxuNuJwV'); + var public1 = privateKey1.publicKey; + var public2 = privateKey2.publicKey; + var public3 = privateKey3.publicKey; + var address = new Address('33zbk2aSZYdNbRsMPPt6jgy6Kq1kQreqeb'); + + var output = { + address: '33zbk2aSZYdNbRsMPPt6jgy6Kq1kQreqeb', + txId: '66e64ef8a3b384164b78453fa8c8194de9a473ba14f89485a0e433699daec140', + outputIndex: 0, + script: new Script(address), + satoshis: 1000000 + }; + it('can count missing signatures', function() { + var transaction = new Transaction() + .from(output, [public1, public2, public3], 2) + .to(address, 1000000); + var input = transaction.inputs[0]; + + input.countSignatures().should.equal(0); + + transaction.sign(privateKey1); + input.countSignatures().should.equal(1); + input.countMissingSignatures().should.equal(1); + input.isFullySigned().should.equal(false); + + transaction.sign(privateKey2); + input.countSignatures().should.equal(2); + input.countMissingSignatures().should.equal(0); + input.isFullySigned().should.equal(true); + }); + it('returns a list of public keys with missing signatures', function() { + var transaction = new Transaction() + .from(output, [public1, public2, public3], 2) + .to(address, 1000000); + var input = transaction.inputs[0]; + + _.all(input.publicKeysWithoutSignature(), function(publicKeyMissing) { + var serialized = publicKeyMissing.toString(); + return serialized === public1.toString() || + serialized === public2.toString() || + serialized === public3.toString(); + }).should.equal(true); + transaction.sign(privateKey1); + _.all(input.publicKeysWithoutSignature(), function(publicKeyMissing) { + var serialized = publicKeyMissing.toString(); + return serialized === public2.toString() || + serialized === public3.toString(); + }).should.equal(true); + }); + it('can clear all signatures', function() { + var transaction = new Transaction() + .from(output, [public1, public2, public3], 2) + .to(address, 1000000) + .sign(privateKey1) + .sign(privateKey2); + + var input = transaction.inputs[0]; + input.isFullySigned().should.equal(true); + input.clearSignatures(); + input.isFullySigned().should.equal(false); + }); + it('can estimate how heavy is the output going to be', function() { + var transaction = new Transaction() + .from(output, [public1, public2, public3], 2) + .to(address, 1000000); + var input = transaction.inputs[0]; + input._estimateSize().should.equal(257); + }); + it('uses SIGHASH_ALL by default', function() { + var transaction = new Transaction() + .from(output, [public1, public2, public3], 2) + .to(address, 1000000); + var input = transaction.inputs[0]; + var sigs = input.getSignatures(transaction, privateKey1, 0); + sigs[0].sigtype.should.equal(Signature.SIGHASH_ALL); + }); +}); diff --git a/test/transaction/input/publickeyhash.js b/test/transaction/input/publickeyhash.js new file mode 100644 index 0000000..f8f19ae --- /dev/null +++ b/test/transaction/input/publickeyhash.js @@ -0,0 +1,65 @@ +'use strict'; +/* jshint unused: false */ + +var should = require('chai').should(); +var expect = require('chai').expect; +var _ = require('lodash'); + +var bitcore = require('../../..'); +var Transaction = bitcore.Transaction; +var PrivateKey = bitcore.PrivateKey; +var Address = bitcore.Address; +var Script = bitcore.Script; +var Networks = bitcore.Networks; +var Signature = bitcore.crypto.Signature; + +describe('PublicKeyHashInput', function() { + + var privateKey = new PrivateKey('KwF9LjRraetZuEjR8VqEq539z137LW5anYDUnVK11vM3mNMHTWb4'); + var publicKey = privateKey.publicKey; + var address = new Address(publicKey, Networks.livenet); + + var output = { + address: '33zbk2aSZYdNbRsMPPt6jgy6Kq1kQreqeb', + txId: '66e64ef8a3b384164b78453fa8c8194de9a473ba14f89485a0e433699daec140', + outputIndex: 0, + script: new Script(address), + satoshis: 1000000 + }; + it('can count missing signatures', function() { + var transaction = new Transaction() + .from(output) + .to(address, 1000000); + var input = transaction.inputs[0]; + + input.isFullySigned().should.equal(false); + transaction.sign(privateKey); + input.isFullySigned().should.equal(true); + }); + it('it\'s size can be estimated', function() { + var transaction = new Transaction() + .from(output) + .to(address, 1000000); + var input = transaction.inputs[0]; + input._estimateSize().should.equal(107); + }); + it('it\'s signature can be removed', function() { + var transaction = new Transaction() + .from(output) + .to(address, 1000000); + var input = transaction.inputs[0]; + + transaction.sign(privateKey); + input.clearSignatures(); + input.isFullySigned().should.equal(false); + }); + it('returns an empty array if private key mismatches', function() { + var transaction = new Transaction() + .from(output) + .to(address, 1000000); + var input = transaction.inputs[0]; + + input.getSignatures(transaction, new PrivateKey(), 0); + input.isFullySigned().should.equal(false); + }); +}); diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index db23b76..b9ae1e2 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -3,6 +3,7 @@ /* jshint unused: false */ /* jshint latedef: false */ var should = require('chai').should(); +var expect = require('chai').expect; var _ = require('lodash'); var bitcore = require('../..'); @@ -11,6 +12,7 @@ var PrivateKey = bitcore.PrivateKey; var Script = bitcore.Script; var Address = bitcore.Address; var Networks = bitcore.Networks; +var errors = bitcore.errors; var transactionVector = require('../data/tx_creation'); @@ -21,6 +23,46 @@ describe('Transaction', function() { transaction.serialize().should.equal(tx_1_hex); }); + it('fails if an invalid parameter is passed to constructor', function() { + expect(function() { + return new Transaction(1); + }).to.throw(errors.InvalidArgument); + }); + + var testScript = 'OP_DUP OP_HASH160 20 0x88d9931ea73d60eaf7e5671efc0552b912911f2a OP_EQUALVERIFY OP_CHECKSIG'; + var testPrevTx = 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458'; + var testAmount = 1020000; + var testTransaction = new Transaction() + .from({ + 'txId': testPrevTx, + 'outputIndex': 0, + 'script': testScript, + 'satoshis': testAmount + }).to('mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc', testAmount - 10000); + + it('can serialize to a plain javascript object', function() { + var object = testTransaction.toObject(); + object.inputs[0].output.satoshis.should.equal(testAmount); + object.inputs[0].output.script.toString().should.equal(testScript); + object.inputs[0].prevTxId.should.equal(testPrevTx); + object.inputs[0].outputIndex.should.equal(0); + object.outputs[0].satoshis.should.equal(testAmount - 10000); + }); + + it('returns the fee correctly', function() { + testTransaction.getFee().should.equal(10000); + }); + + it('serialize to Object roundtrip', function() { + new Transaction(testTransaction.toObject()).serialize().should.equal(testTransaction.serialize()); + }); + + it('constructor returns a shallow copy of another transaction', function() { + var transaction = new Transaction(tx_1_hex); + var copy = new Transaction(transaction); + copy.serialize().should.equal(transaction.serialize()); + }); + it('should display correctly in console', function() { var transaction = new Transaction(tx_1_hex); transaction.inspect().should.equal(''); @@ -63,76 +105,21 @@ describe('Transaction', function() { }); // TODO: Migrate this into a test for inputs - describe('MultiSigScriptHashInput', function() { - var MultiSigScriptHashInput = Transaction.Input.MultiSigScriptHash; - var privateKey1 = new PrivateKey('KwF9LjRraetZuEjR8VqEq539z137LW5anYDUnVK11vM3mNMHTWb4'); - var privateKey2 = new PrivateKey('L4PqnaPTCkYhAqH3YQmefjxQP6zRcF4EJbdGqR8v6adtG9XSsadY'); - var privateKey3 = new PrivateKey('L4CTX79zFeksZTyyoFuPQAySfmP7fL3R41gWKTuepuN7hxuNuJwV'); - var public1 = privateKey1.publicKey; - var public2 = privateKey2.publicKey; - var public3 = privateKey3.publicKey; - var address = new Address('33zbk2aSZYdNbRsMPPt6jgy6Kq1kQreqeb'); + var fromAddress = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1'; + var simpleUtxoWith100000Satoshis = { + address: fromAddress, + txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', + outputIndex: 0, + script: Script.buildPublicKeyHashOut(fromAddress).toString(), + satoshis: 100000 + }; + var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc'; + var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up'; + var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf'; + var privateKey = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY'; - var output = { - address: '33zbk2aSZYdNbRsMPPt6jgy6Kq1kQreqeb', - txId: '66e64ef8a3b384164b78453fa8c8194de9a473ba14f89485a0e433699daec140', - outputIndex: 0, - script: new Script(address), - satoshis: 1000000 - }; - it('can count missing signatures', function() { - var transaction = new Transaction() - .from(output, [public1, public2, public3], 2) - .to(address, 1000000); - var input = transaction.inputs[0]; - - input.countSignatures().should.equal(0); - - transaction.sign(privateKey1); - input.countSignatures().should.equal(1); - input.countMissingSignatures().should.equal(1); - input.isFullySigned().should.equal(false); - - transaction.sign(privateKey2); - input.countSignatures().should.equal(2); - input.countMissingSignatures().should.equal(0); - input.isFullySigned().should.equal(true); - }); - it('returns a list of public keys with missing signatures', function() { - var transaction = new Transaction() - .from(output, [public1, public2, public3], 2) - .to(address, 1000000); - var input = transaction.inputs[0]; - - _.all(input.publicKeysWithoutSignature(), function(publicKeyMissing) { - var serialized = publicKeyMissing.toString(); - return serialized === public1.toString() || - serialized === public2.toString() || - serialized === public3.toString(); - }).should.equal(true); - transaction.sign(privateKey1); - _.all(input.publicKeysWithoutSignature(), function(publicKeyMissing) { - var serialized = publicKeyMissing.toString(); - return serialized === public2.toString() || - serialized === public3.toString(); - }).should.equal(true); - }); - }); describe('change address', function() { - var fromAddress = 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1'; - var simpleUtxoWith100000Satoshis = { - address: fromAddress, - txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', - outputIndex: 0, - script: Script.buildPublicKeyHashOut(fromAddress).toString(), - satoshis: 100000 - }; - var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc'; - var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up'; - var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf'; - var privateKey = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY'; - it('can calculate simply the output amount', function() { var transaction = new Transaction() .from(simpleUtxoWith100000Satoshis) @@ -140,7 +127,7 @@ describe('Transaction', function() { .change(changeAddress) .sign(privateKey); transaction.outputs.length.should.equal(2); - transaction.outputs[1].satoshis.should.equal(49000); + transaction.outputs[1].satoshis.should.equal(40000); transaction.outputs[1].script.toString() .should.equal(Script.fromAddress(changeAddress).toString()); }); @@ -162,7 +149,7 @@ describe('Transaction', function() { .to(toAddress, 20000) .sign(privateKey); transaction.outputs.length.should.equal(3); - transaction.outputs[2].satoshis.should.equal(29000); + transaction.outputs[2].satoshis.should.equal(20000); transaction.outputs[2].script.toString() .should.equal(Script.fromAddress(changeAddress).toString()); }); @@ -201,6 +188,34 @@ describe('Transaction', function() { transaction.outputs.length.should.equal(1); }); }); + + var simpleUtxoWith1BTC = { + address: fromAddress, + txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', + outputIndex: 0, + script: Script.buildPublicKeyHashOut(fromAddress).toString(), + satoshis: 1e8 + }; + + describe('checked serialize', function() { + it('fails if no change address was set', function() { + var transaction = new Transaction() + .from(simpleUtxoWith1BTC) + .to(toAddress, 1); + expect(function() { + return transaction.serialize(); + }).to.throw(errors.Transaction.ChangeAddressMissing); + }); + it('fails if a high fee was set', function() { + var transaction = new Transaction() + .from(simpleUtxoWith1BTC) + .change(changeAddress) + .to(toAddress, 1); + expect(function() { + return transaction.serialize(); + }).to.throw(errors.Transaction.FeeError); + }); + }); }); var tx_empty_hex = '01000000000000000000';