diff --git a/lib/transaction/input/input.js b/lib/transaction/input/input.js index b059edf..b18a6b2 100644 --- a/lib/transaction/input/input.js +++ b/lib/transaction/input/input.js @@ -132,6 +132,10 @@ Input.prototype.addSignature = function() { throw new errors.AbstractMethodInvoked('Input#addSignature'); }; +Input.prototype.clearSignatures = function() { + throw new errors.AbstractMethodInvoked('Input#clearSignatures'); +}; + Input.prototype.isValidSignature = function(transaction, signature) { // FIXME: Refactor signature so this is not necessary signature.signature.nhashtype = signature.sigtype; diff --git a/lib/transaction/input/multisigscripthash.js b/lib/transaction/input/multisigscripthash.js index 6640d36..bb60357 100644 --- a/lib/transaction/input/multisigscripthash.js +++ b/lib/transaction/input/multisigscripthash.js @@ -124,4 +124,11 @@ MultiSigScriptHashInput.prototype.isValidSignature = function(transaction, signa ); }; +MultiSigScriptHashInput.OPCODES_SIZE = 10; +MultiSigScriptHashInput.SIGNATURE_SIZE = 36; + +MultiSigScriptHashInput.prototype._estimateSize = function() { + return MultiSigScriptHashInput.OPCODES_SIZE + this.threshold * MultiSigScriptHashInput.SIGNATURE_SIZE; +}; + module.exports = MultiSigScriptHashInput; diff --git a/lib/transaction/input/publickeyhash.js b/lib/transaction/input/publickeyhash.js index bfebd83..cdd8c06 100644 --- a/lib/transaction/input/publickeyhash.js +++ b/lib/transaction/input/publickeyhash.js @@ -72,7 +72,7 @@ PublicKeyHashInput.prototype.addSignature = function(transaction, signature) { * Clear the input's signature * @return {PublicKeyHashInput} this, for chaining */ -PublicKeyHashInput.prototype.clearSignature = function() { +PublicKeyHashInput.prototype.clearSignatures = function() { this.setScript(Script.empty()); return this; }; @@ -85,4 +85,10 @@ PublicKeyHashInput.prototype.isFullySigned = function() { return this.script.isPublicKeyHashIn(); }; +PublicKeyHashInput.FIXED_SIZE = 32 + 4 + 2; +PublicKeyHashInput.SCRIPT_MAX_SIZE = 34 + 20; +PublicKeyHashInput.prototype._estimateSize = function() { + return PublicKeyHashInput.FIXED_SIZE + PublicKeyHashInput.SCRIPT_MAX_SIZE; +}; + module.exports = PublicKeyHashInput; diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index ca8bc8a..7e2a204 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -1,6 +1,7 @@ 'use strict'; var _ = require('lodash'); +var $ = require('../util/preconditions'); var buffer = require('buffer'); var assert = require('assert'); @@ -27,8 +28,7 @@ var DEFAULT_NLOCKTIME = 0; var DEFAULT_SEQNUMBER = 0xFFFFFFFF; /** - * Represents a transaction, a set of inputs and outputs to change - * ownership of tokens + * Represents a transaction, a set of inputs and outputs to change ownership of tokens * * @param {*} serialized * @constructor @@ -196,6 +196,45 @@ Transaction.prototype._newTransaction = function() { /* Transaction creation interface */ +/** + * Add an input to this transaction. This is a high level interface + * to add an input, for more control, use @{link Transaction#addInput}. + * + * Can receive, as output information, the output of bitcoind's `listunspent` command, + * and a slightly fancier format recognized by bitcore: + * + * ``` + * { + * address: 'mszYqVnqKoQx4jcTdJXxwKAissE3Jbrrc1', + * txId: 'a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458', + * outputIndex: 0, + * script: Script.empty(), + * satoshis: 1020000 + * } + * ``` + * Where `address` can be either a string or a bitcore Address object. The + * same is true for `script`, which can be a string or a bitcore Script. + * + * Beware that this resets all the signatures for inputs (in further versions, + * SIGHASH_SINGLE or SIGHASH_NONE signatures will not be reset). + * + * @example + * var transaction = new Transaction(); + * + * // From a pay to public key hash output from bitcoind's listunspent + * transaction.from({'txid': '0000...', vout: 0, amount: 0.1, scriptPubKey: 'OP_DUP ...'}); + * + * // From a pay to public key hash output + * transaction.from({'txId': '0000...', outputIndex: 0, satoshis: 1000, script: 'OP_DUP ...'}); + * + * // From a multisig P2SH output + * transaction.from({'txId': '0000...', inputIndex: 0, satoshis: 1000, script: '... OP_HASH'}, + * ['03000...', '02000...'], 2); + * + * @param {Object} utxo + * @param {Array=} pubkeys + * @param {number=} threshold + */ Transaction.prototype.from = function(utxo, pubkeys, threshold) { if (pubkeys && threshold) { this._fromMultiSigP2SH(utxo, pubkeys, threshold); @@ -283,6 +322,7 @@ 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({ @@ -298,24 +338,58 @@ Transaction.prototype._fromMultisigNewUtxo = function(utxo, pubkeys, threshold) this._inputAmount += utxo.satoshis; }; +/** + * Returns true if the transaction has enough info on all inputs to be correctly validated + * + * @return {boolean} + */ Transaction.prototype.hasAllUtxoInfo = function() { return _.all(this.inputs.map(function(input) { return !!input.output; })); }; +/** + * Manually set the fee for this transaction. Beware that this resets all the signatures + * for inputs (in further versions, SIGHASH_SINGLE or SIGHASH_NONE signatures will not + * be reset). + * + * @param {number} amount satoshis to be sent + * @return {Transaction} this, for chaining + */ Transaction.prototype.fee = function(amount) { this._fee = amount; + this._changeSetup = false; return this; }; /* Output management */ +/** + * Set the change address for this transaction + * + * Beware that this resets all the signatures for inputs (in further versions, + * SIGHASH_SINGLE or SIGHASH_NONE signatures will not be reset). + * + * @param {number} amount satoshis to be sent + * @return {Transaction} this, for chaining + */ Transaction.prototype.change = function(address) { - this._change = address; + this._change = new Address(address); + this._changeSetup = false; return this; }; +/** + * Add an output to the transaction. + * + * Beware that this resets all the signatures for inputs (in further versions, + * SIGHASH_SINGLE or SIGHASH_NONE signatures will not be reset). + * + * @param {string|Address} address + * @param {number} amount in satoshis + * @return {Transaction} this, for chaining + */ Transaction.prototype.to = function(address, amount) { this._addOutput(new Output({ script: Script(new Address(address)), @@ -324,11 +398,16 @@ Transaction.prototype.to = function(address, amount) { return this; }; -Transaction.prototype._addOutput = function(output) { - this.outputs.push(output); - this._outputAmount += output.satoshis; -}; - +/** + * Add an OP_RETURN output to the transaction. + * + * Beware that this resets all the signatures for inputs (in further versions, + * SIGHASH_SINGLE or SIGHASH_NONE signatures will not be reset). + * + * @param {Buffer|string} value the data to be stored in the OP_RETURN output. + * In case of a string, the UTF-8 representation will be stored + * @return {Transaction} this, for chaining + */ Transaction.prototype.addData = function(value) { this._addOutput(new Output({ script: Script.buildDataOut(value), @@ -337,19 +416,100 @@ Transaction.prototype.addData = function(value) { return this; }; +Transaction.prototype._addOutput = function(output) { + this.outputs.push(output); + this._changeSetup = false; + this._outputAmount += output.satoshis; +}; + +Transaction.prototype._updateChangeOutput = function() { + if (!this._change) { + return; + } + if (this._changeSetup) { + return; + } + if (!_.isUndefined(this._changeSetup)) { + this._clearSignatures(); + } + 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); + if (available - fee > 0) { + this._changeOutput = this.outputs.length; + this._addOutput(new Output({ + script: Script.fromAddress(this._change), + satoshis: available - fee + })); + } else { + this._changeOutput = undefined; + } + this._changeSetup = true; +}; + +Transaction.prototype._clearSignatures = function() { + _.each(this.inputs, function(input) { + input.clearSignatures(); + }); +}; + +Transaction.FEE_PER_KB = 10000; +Transaction.CHANGE_OUTPUT_MAX_SIZE = 20 + 4 + 34 + 4; + +Transaction._estimateFee = function(size, amountAvailable) { + var fee = Math.ceil(size / Transaction.FEE_PER_KB); + if (amountAvailable > fee) { + // Safe upper bound for change address script + size += Transaction.CHANGE_OUTPUT_MAX_SIZE; + } + return Math.ceil(size / 1000 / Transaction.FEE_PER_KB) * 1000; +}; + +Transaction.MAXIMUM_EXTRA_SIZE = 4 + 9 + 9 + 4; + +Transaction.prototype._estimateSize = function() { + var result = Transaction.MAXIMUM_EXTRA_SIZE; + _.each(this.inputs, function(input) { + result += input._estimateSize(); + }); + _.each(this.outputs, function(output) { + result += output.script.toBuffer().length + 9; + }); + return result; +}; + +Transaction.prototype.removeOutput = function(index) { + var output = this.outputs[index]; + this._outputAmount -= output.satoshis; + this.outputs = _.without(this.outputs, this.outputs[this._changeOutput]); +}; + /* Signature handling */ -Transaction.prototype.sign = function(privKey, sigtype) { - // TODO: Change for preconditions - assert(this.hasAllUtxoInfo()); +/** + * Sign the transaction using one or more private keys. + * + * It tries to sign each input, verifying that the signature will be valid + * (matches a public key). + * + * @param {Array|String|PrivateKey} privateKey + * @param {number} sigtype + * @return {Transaction} this, for chaining + */ +Transaction.prototype.sign = function(privateKey, sigtype) { + $.checkState(this.hasAllUtxoInfo()); + this._updateChangeOutput(); var self = this; - if (_.isArray(privKey)) { - _.each(privKey, function(privKey) { - self.sign(privKey); + if (_.isArray(privateKey)) { + _.each(privateKey, function(privateKey) { + self.sign(privateKey); }); return this; } - _.each(this.getSignatures(privKey, sigtype), function(signature) { + _.each(this.getSignatures(privateKey, sigtype), function(signature) { self.applySignature(signature); }); return this; @@ -369,6 +529,16 @@ Transaction.prototype._getPrivateKeySignatures = function(privKey, sigtype) { return results; }; +/** + * Add a signature to the transaction + * + * @param {Object} signature + * @param {number} signature.inputIndex + * @param {number} signature.sighash + * @param {PublicKey} signature.publicKey + * @param {Signature} signature.signature + * @return {Transaction} this, for chaining + */ Transaction.prototype.applySignature = function(signature) { this.inputs[signature.inputIndex].addSignature(this, signature); return this; diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 22223c0..c506a37 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -119,6 +119,78 @@ describe('Transaction', function() { }).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 privateKey = 'cSBnVM4xvxarwGQuAfQFwqDg9k5tErHUHzgWsEfD4zdwUasvqRVY'; + + it('can calculate simply the output amount', function() { + var transaction = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to(toAddress, 50000) + .change(changeAddress) + .sign(privateKey); + transaction.outputs.length.should.equal(2); + transaction.outputs[1].satoshis.should.equal(49000); + transaction.outputs[1].script.toString() + .should.equal(Script.fromAddress(changeAddress).toString()); + }); + it('can recalculate the change amount', function() { + var transaction = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to(toAddress, 50000) + .change(changeAddress) + .sign(privateKey) + .to(toAddress, 20000) + .sign(privateKey); + transaction.outputs.length.should.equal(3); + transaction.outputs[2].satoshis.should.equal(29000); + transaction.outputs[2].script.toString() + .should.equal(Script.fromAddress(changeAddress).toString()); + }); + it('adds no fee if no change is available', function() { + var transaction = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to(toAddress, 99000) + .sign(privateKey); + transaction.outputs.length.should.equal(1); + }); + it('adds no fee if no money is available', function() { + var transaction = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to(toAddress, 100000) + .change(changeAddress) + .sign(privateKey); + transaction.outputs.length.should.equal(1); + }); + it('fee can be set up manually', function() { + var transaction = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to(toAddress, 80000) + .fee(10000) + .change(changeAddress) + .sign(privateKey); + transaction.outputs.length.should.equal(2); + transaction.outputs[1].satoshis.should.equal(10000); + }); + it('coverage: on second call to sign, change is not recalculated', function() { + var transaction = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to(toAddress, 100000) + .change(changeAddress) + .sign(privateKey) + .sign(privateKey); + transaction.outputs.length.should.equal(1); + }); + }); }); var tx_empty_hex = '01000000000000000000';