From 07c8c153f98f8ed9becec1f2205ca31549f530ce Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 28 Jan 2015 12:19:02 -0300 Subject: [PATCH 01/10] add Transaction#lockUntil stub --- lib/transaction/transaction.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 73b945e..74e06fb 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -279,6 +279,16 @@ Transaction.prototype.fromObject = function(transaction) { this.version = transaction.version; }; +/** + * sets nLockTime so that transaction is not valid until + * the desired date or block height + * @param {Date | Number} time + */ +Transaction.prototype.lockUntil = function(time) { + $.checkArgument(time); + this.nLockTime = DEFAULT_NLOCKTIME; +}; + Transaction.prototype.toJSON = function toJSON() { return JSON.stringify(this.toObject()); }; From 7cf3b6eb9168d5e96dd2cd545941f962c254fd90 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Mon, 9 Feb 2015 14:57:45 -0300 Subject: [PATCH 02/10] Split setLockTime in two methods --- lib/errors/spec.js | 9 +++++++ lib/transaction/transaction.js | 42 +++++++++++++++++++++++++++++---- test/transaction/transaction.js | 40 +++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/lib/errors/spec.js b/lib/errors/spec.js index c2dd28a..eece532 100644 --- a/lib/errors/spec.js +++ b/lib/errors/spec.js @@ -72,6 +72,15 @@ module.exports = [{ }, { name: 'ChangeAddressMissing', message: 'Change address is missing' + }, { + name: 'BlockHeightTooHigh', + message: 'Block Height can be at most 2^32 -1' + }, { + name: 'NLockTimeOutOfRange', + message: 'Block Height can only be between 0 and 499 999 999' + }, { + name: 'LockTimeTooEarly', + message: 'Lock Time can\'t be earlier than UNIX date 500 000 000' }] }, { name: 'Script', diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 74e06fb..ef28e0a 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -66,6 +66,12 @@ function Transaction(serialized) { // max amount of satoshis in circulation Transaction.MAX_MONEY = 21000000 * 1e8; +// nlocktime limit to be considered block height rather than a timestamp +Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT = 5e8; + +// Max value for an unsigned 32 bit value +Transaction.NLOCKTIME_MAX_VALUE = 4294967295; + /* Constructors and Serialization */ /** @@ -280,13 +286,41 @@ Transaction.prototype.fromObject = function(transaction) { }; /** - * sets nLockTime so that transaction is not valid until - * the desired date or block height + * Sets nLockTime so that transaction is not valid until the desired date(a + * timestamp in seconds since UNIX epoch is also accepted) + * * @param {Date | Number} time + * @return {Transaction} this */ -Transaction.prototype.lockUntil = function(time) { +Transaction.prototype.lockUntilDate = function(time) { $.checkArgument(time); - this.nLockTime = DEFAULT_NLOCKTIME; + if (_.isNumber(time) && time < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) { + throw new errors.Transaction.LockTimeTooEarly(); + } + if (_.isDate(time)) { + time = time.getTime() / 1000; + } + this.nLockTime = time; + return this; +}; + +/** + * Sets nLockTime so that transaction is not valid until the desired block + * height. + * + * @param {Number} time + * @return {Transaction} this + */ +Transaction.prototype.lockUntilBlockHeight = function(time) { + $.checkArgument(_.isNumber(time)); + if (time >= Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) { + throw new errors.Transaction.BlockHeightTooHigh(); + } + if (time < 0) { + throw new errors.Transaction.NLockTimeOutOfRange(); + } + this.nLockTime = time; + return this; }; Transaction.prototype.toJSON = function toJSON() { diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index dbbd348..56bbee1 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -407,6 +407,46 @@ describe('Transaction', function() { transaction.outputs.length.should.equal(1); }); }); + + describe.only('setting the nLockTime', function() { + var MILLIS_IN_SECOND = 1000; + var timestamp = 1423504946; + var blockHeight = 342734; + var date = new Date(timestamp * MILLIS_IN_SECOND); + it('accepts a date instance', function() { + var transaction = new Transaction() + .lockUntilDate(date); + transaction.nLockTime.should.equal(timestamp); + }); + it('accepts a number instance with a timestamp', function() { + var transaction = new Transaction() + .lockUntilDate(timestamp); + transaction.nLockTime.should.equal(timestamp); + }); + it('accepts a block height', function() { + var transaction = new Transaction() + .lockUntilBlockHeight(blockHeight); + transaction.nLockTime.should.equal(blockHeight); + }); + it('fails if the block height is too high', function() { + expect(function() { + return new Transaction().lockUntilBlockHeight(5e8); + }).to.throw(errors.Transaction.BlockHeightTooHigh); + }); + it('fails if the date is too early', function() { + expect(function() { + return new Transaction().lockUntilDate(1); + }).to.throw(errors.Transaction.LockTimeTooEarly); + expect(function() { + return new Transaction().lockUntilDate(499999999); + }).to.throw(errors.Transaction.LockTimeTooEarly); + }); + it('fails if the date is negative', function() { + expect(function() { + return new Transaction().lockUntilBlockHeight(-1); + }).to.throw(errors.Transaction.NLockTimeOutOfRange); + }); + }); }); var tx_empty_hex = '01000000000000000000'; From f8974b383ab8e74160b2b5618a01eb80fbb21a33 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Tue, 10 Feb 2015 10:11:09 -0300 Subject: [PATCH 03/10] transaction: Change variable name from time to height --- lib/transaction/transaction.js | 12 ++++++------ test/transaction/transaction.js | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index ef28e0a..09e7250 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -308,18 +308,18 @@ Transaction.prototype.lockUntilDate = function(time) { * Sets nLockTime so that transaction is not valid until the desired block * height. * - * @param {Number} time + * @param {Number} height * @return {Transaction} this */ -Transaction.prototype.lockUntilBlockHeight = function(time) { - $.checkArgument(_.isNumber(time)); - if (time >= Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) { +Transaction.prototype.lockUntilBlockHeight = function(height) { + $.checkArgument(_.isNumber(height)); + if (height >= Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) { throw new errors.Transaction.BlockHeightTooHigh(); } - if (time < 0) { + if (height < 0) { throw new errors.Transaction.NLockTimeOutOfRange(); } - this.nLockTime = time; + this.nLockTime = height; return this; }; diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 56bbee1..db0e81f 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -408,7 +408,7 @@ describe('Transaction', function() { }); }); - describe.only('setting the nLockTime', function() { + describe('setting the nLockTime', function() { var MILLIS_IN_SECOND = 1000; var timestamp = 1423504946; var blockHeight = 342734; From 986264e18102913d2f46f9f435993aa0ee9a886e Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Tue, 10 Feb 2015 13:03:07 -0300 Subject: [PATCH 04/10] add locktime docs and Transaction#getLockTime --- docs/transaction.md | 20 ++++++++++++++++++++ lib/transaction/transaction.js | 17 +++++++++++++++++ test/transaction/transaction.js | 16 +++++++++++++--- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/docs/transaction.md b/docs/transaction.md index a669eca..29b794d 100644 --- a/docs/transaction.md +++ b/docs/transaction.md @@ -140,6 +140,26 @@ var multiSigTx = new Transaction(serialized) assert(multiSigTx.isFullySigned()); ``` +## Time-Locking transaction +All bitcoin transactions contain a locktime field. +The locktime indicates the earliest time a transaction can be added to the blockchain. +Locktime allows signers to create time-locked transactions which will only become valid in the future, giving the signers a chance to change their minds. +Locktime can be set in the form of a bitcoin block height (the transaction can only be included in a block with a higher height than specified) or a linux timestamp (transaction can only be confirmed after that time). +For more information see [bitcoin's development guide section on locktime](https://bitcoin.org/en/developer-guide#locktime-and-sequence-number). + +In bitcore, you can set a `Transaction`'s locktime by using the methods `Transaction#lockUntilDate` and `Transaction#lockUntilBlockHeight`. You can also get a friendly version of the locktime field via `Transaction#getLockTime`; + +For example: +```javascript +var future = new Date(2025,10,30); // Sun Nov 30 2025 +var transaction = new Transaction() + .lockUntilDate(future); +console.log(transaction.getLockTime()); +// output similar to: Sun Nov 30 2025 00:00:00 GMT-0300 (ART) +``` + + + ## Upcoming changes We're debating an API for Merge Avoidance, CoinJoin, Smart contracts, CoinSwap, and Stealth Addresses. We're expecting to have all of them by some time in 2015. Payment channel creation is avaliable in the [bitcore-channel](https://github.com/bitpay/bitcore-channel) module. diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 09e7250..e0edb54 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -323,6 +323,23 @@ Transaction.prototype.lockUntilBlockHeight = function(height) { return this; }; +/** + * Returns a semantic version of the transaction's nLockTime. + * @return {Number|Date} + * If nLockTime is 0, it returns null, + * if it is < 500000000, it returns a block height (number) + * else it returns a Date object. + */ +Transaction.prototype.getLockTime = function() { + if (!this.nLockTime) { + return null; + } + if (this.nLockTime < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) { + return this.nLockTime; + } + return new Date(1000*this.nLockTime); +}; + Transaction.prototype.toJSON = function toJSON() { return JSON.stringify(this.toObject()); }; diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index db0e81f..c54571b 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -135,7 +135,7 @@ describe('Transaction', function() { describe('adding inputs', function() { - it('it only adds once one utxo', function() { + it('only adds once one utxo', function() { var tx = new Transaction(); tx.from(simpleUtxoWith1BTC); tx.from(simpleUtxoWith1BTC); @@ -408,25 +408,35 @@ describe('Transaction', function() { }); }); - describe('setting the nLockTime', function() { + describe('handling the nLockTime', function() { var MILLIS_IN_SECOND = 1000; var timestamp = 1423504946; var blockHeight = 342734; var date = new Date(timestamp * MILLIS_IN_SECOND); + it('handles a simple example', function() { + var future = new Date(2025,10,30); // Sun Nov 30 2025 + var transaction = new Transaction() + .lockUntilDate(future); + transaction.nLockTime.should.equal(future.getTime()/1000); + transaction.getLockTime().should.deep.equal(future); + }); it('accepts a date instance', function() { var transaction = new Transaction() .lockUntilDate(date); transaction.nLockTime.should.equal(timestamp); + transaction.getLockTime().should.deep.equal(date); }); it('accepts a number instance with a timestamp', function() { var transaction = new Transaction() .lockUntilDate(timestamp); transaction.nLockTime.should.equal(timestamp); + transaction.getLockTime().should.deep.equal(new Date(timestamp*1000)); }); it('accepts a block height', function() { var transaction = new Transaction() .lockUntilBlockHeight(blockHeight); transaction.nLockTime.should.equal(blockHeight); + transaction.getLockTime().should.deep.equal(blockHeight); }); it('fails if the block height is too high', function() { expect(function() { @@ -441,7 +451,7 @@ describe('Transaction', function() { return new Transaction().lockUntilDate(499999999); }).to.throw(errors.Transaction.LockTimeTooEarly); }); - it('fails if the date is negative', function() { + it('fails if the block height is negative', function() { expect(function() { return new Transaction().lockUntilBlockHeight(-1); }).to.throw(errors.Transaction.NLockTimeOutOfRange); From aa1158097d09814d80393f4f77f5ee23f561da41 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Tue, 10 Feb 2015 18:12:45 -0300 Subject: [PATCH 05/10] fix bug in change calculation --- docs/transaction.md | 4 +- lib/transaction/input/input.js | 7 +- lib/transaction/transaction.js | 111 +++++++++++++++++++------------- test/transaction/transaction.js | 41 +++++++++--- 4 files changed, 104 insertions(+), 59 deletions(-) diff --git a/docs/transaction.md b/docs/transaction.md index 29b794d..0c4f04e 100644 --- a/docs/transaction.md +++ b/docs/transaction.md @@ -112,11 +112,11 @@ When outputs' value don't sum up to the same amount that inputs, the difference For this reason, some methods in the Transaction class are provided: -* `change(address)`: Set up the change address. This will set an internal `_change` property that will store the change address. +* `change(address)`: Set up the change address. This will set an internal `_changeScript` property that will store the change script associated with that address. * `fee(amount)`: Sets up the exact amount of fee to pay. If no change address is provided, this will raise an exception. * `getFee()`: returns the estimated fee amount to be paid, based on the size of the transaction, but disregarding the priority of the outputs. -Internally, a `_changeOutput` property stores the index of the change output (so it can get updated when a new input or output is added). +Internally, a `_changeIndex` property stores the index of the change output (so it can get updated when a new input or output is added). ## Multisig Transactions diff --git a/lib/transaction/input/input.js b/lib/transaction/input/input.js index 1255a71..47a5c8c 100644 --- a/lib/transaction/input/input.js +++ b/lib/transaction/input/input.js @@ -36,15 +36,14 @@ Input.prototype._fromObject = function(params) { params.prevTxId = new buffer.Buffer(params.prevTxId, 'hex'); } this.output = params.output ? - (params.output instanceof Output ? params.output : new Output(params.output)) : undefined; + (params.output instanceof Output ? params.output : new Output(params.output)) : undefined; this.prevTxId = params.prevTxId; this.outputIndex = params.outputIndex; this.sequenceNumber = params.sequenceNumber; - if (!_.isUndefined(params.script) || !_.isUndefined(params.scriptBuffer)) { - this.setScript(_.isUndefined(params.script) ? params.scriptBuffer : params.script); - } else { + if (_.isUndefined(params.script) && _.isUndefined(params.scriptBuffer)) { throw new errors.Transaction.Input.MissingScript(); } + this.setScript(params.scriptBuffer || params.script); return this; }; diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index e0edb54..8b6a46d 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -126,13 +126,12 @@ Transaction.prototype.uncheckedSerialize = function() { 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); - } + var missingChange = this._missingChange(); + if (feeError && missingChange) { + throw new errors.Transaction.ChangeAddressMissing(); + } + if (feeError && !missingChange) { + throw new errors.Transaction.FeeError(feeError); } if (this._hasDustOutputs()) { throw new errors.Transaction.DustOutputs(); @@ -148,10 +147,8 @@ Transaction.prototype._validateFees = function() { } }; -Transaction.prototype._validateChange = function() { - if (!this._change) { - return 'Missing change address'; - } +Transaction.prototype._missingChange = function() { + return !this._changeScript; }; Transaction.DUST_AMOUNT = 5460; @@ -217,23 +214,7 @@ Transaction.prototype.fromJSON = function(json) { if (JSUtil.isValidJSON(json)) { json = JSON.parse(json); } - var self = this; - this.inputs = []; - var inputs = json.inputs || json.txins; - inputs.forEach(function(input) { - self.inputs.push(Input.fromJSON(input)); - }); - this.outputs = []; - var outputs = json.outputs || json.txouts; - outputs.forEach(function(output) { - self.outputs.push(Output.fromJSON(output)); - }); - if (json.change) { - this.change(json.change); - } - this.version = json.version; - this.nLockTime = json.nLockTime; - return this; + return this.fromObject(json); }; Transaction.prototype.toObject = function toObject() { @@ -246,7 +227,8 @@ Transaction.prototype.toObject = function toObject() { outputs.push(output.toObject()); }); return { - change: this._change ? this._change.toString() : undefined, + changeScript: this._changeScript ? this._changeScript.toString() : undefined, + changeIndex: !_.isUndefined(this._changeIndex) ? this._changeIndex : undefined, fee: this._fee ? this._fee : undefined, version: this.version, inputs: inputs, @@ -275,21 +257,36 @@ Transaction.prototype.fromObject = function(transaction) { _.each(transaction.outputs, function(output) { self.addOutput(new Output(output)); }); - if (transaction.change) { - this.change(transaction.change); + if (transaction.changeIndex) { + this._changeIndex = transaction.changeIndex; + } + if (transaction.changeScript) { + this._changeScript = new Script(transaction.changeScript); } if (transaction.fee) { this.fee(transaction.fee); } this.nLockTime = transaction.nLockTime; this.version = transaction.version; + this._checkConsistency(); + return this; +}; + +Transaction.prototype._checkConsistency = function() { + if (!_.isUndefined(this._changeIndex)) { + $.checkState(this._changeScript); + $.checkState(this.outputs[this._changeIndex]); + $.checkState(this.outputs[this._changeIndex].script.toString() === + this._changeScript.toString()); + } + // TODO: add other checks }; /** * Sets nLockTime so that transaction is not valid until the desired date(a * timestamp in seconds since UNIX epoch is also accepted) * - * @param {Date | Number} time + * @param {Date | Number} time * @return {Transaction} this */ Transaction.prototype.lockUntilDate = function(time) { @@ -337,7 +334,7 @@ Transaction.prototype.getLockTime = function() { if (this.nLockTime < Transaction.NLOCKTIME_BLOCKHEIGHT_LIMIT) { return this.nLockTime; } - return new Date(1000*this.nLockTime); + return new Date(1000 * this.nLockTime); }; Transaction.prototype.toJSON = function toJSON() { @@ -533,11 +530,22 @@ Transaction.prototype.fee = function(amount) { * @return {Transaction} this, for chaining */ Transaction.prototype.change = function(address) { - this._change = new Address(address); + this._changeScript = Script.fromAddress(address); this._updateChangeOutput(); return this; }; + +/** + * @return {Output} change output, if it exists + */ +Transaction.prototype.getChangeOutput = function() { + if (!_.isUndefined(this._changeIndex)) { + return this.outputs[this._changeIndex]; + } + return null; +}; + /** * Add an output to the transaction. * @@ -586,33 +594,46 @@ Transaction.prototype._addOutput = function(output) { }; Transaction.prototype._updateChangeOutput = function() { - if (!this._change) { + if (!this._changeScript) { return; } this._clearSignatures(); - if (!_.isUndefined(this._changeOutput)) { - this._removeOutput(this._changeOutput); + if (!_.isUndefined(this._changeIndex)) { + this._removeOutput(this._changeIndex); } var available = this._getUnspentValue(); var fee = this.getFee(); - if (available - fee > 0) { - this._changeOutput = this.outputs.length; + var changeAmount = available - fee; + if (changeAmount > 0) { + this._changeIndex = this.outputs.length; this._addOutput(new Output({ - script: Script.fromAddress(this._change), - satoshis: available - fee + script: this._changeScript, + satoshis: changeAmount })); } else { - this._changeOutput = undefined; + this._changeIndex = undefined; } }; - +/** + * Calculates the fees for the transaction. + * + * If there is no change output set, the fee will be the + * output amount minus the input amount. + * If there's a fixed fee set, return that + * If there's no fee set, estimate it based on size + * @return {Number} miner fee for this transaction in satoshis + */ Transaction.prototype.getFee = function() { - if (!this._change) { + // if no change output is set, fees should equal all the unspent amount + if (!this._changeScript) { return this._getUnspentValue(); } return this._fee || this._estimateFee(); }; +/** + * Estimates fee from serialized transaction size in bytes. + */ Transaction.prototype._estimateFee = function() { var estimatedSize = this._estimateSize(); var available = this._getUnspentValue(); @@ -630,12 +651,12 @@ Transaction.prototype._clearSignatures = function() { }; Transaction.FEE_PER_KB = 10000; +// Safe upper bound for change address script 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; diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index c54571b..85282ec 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -142,7 +142,16 @@ describe('Transaction', function() { tx.inputs.length.should.equal(1); }); - describe('not enough information errors', function() { + describe('isFullySigned', function() { + it('works for normal p2pkh', function() { + var transaction = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to(toAddress, 50000) + .change(changeAddress) + .sign(privateKey); + transaction.isFullySigned().should.equal(true); + }); + it('fails when Inputs are not subclassed and isFullySigned is called', function() { var tx = new Transaction(tx_1_hex); expect(function() { @@ -172,6 +181,7 @@ describe('Transaction', function() { transaction.outputs[1].satoshis.should.equal(40000); transaction.outputs[1].script.toString() .should.equal(Script.fromAddress(changeAddress).toString()); + transaction.getChangeOutput().script.should.deep.equal(Script.fromAddress(changeAddress)); }); it('accepts a P2SH address for change', function() { var transaction = new Transaction() @@ -243,7 +253,8 @@ describe('Transaction', function() { .change(changeAddress) .toObject(); var deserialized = new Transaction(serialized); - expect(deserialized._change.toString()).to.equal(changeAddress); + expect(deserialized._changeScript.toString()).to.equal(Script.fromAddress(changeAddress).toString()); + expect(deserialized.getChangeOutput()).to.equal(null); }); it('can avoid checked serialize', function() { var transaction = new Transaction() @@ -309,13 +320,23 @@ describe('Transaction', function() { describe('to and from JSON', function() { it('takes a string that is a valid JSON and deserializes from it', function() { - var transaction = new Transaction(); - expect(new Transaction(transaction.toJSON()).serialize()).to.equal(transaction.serialize()); + var simple = new Transaction(); + expect(new Transaction(simple.toJSON()).serialize()).to.equal(simple.serialize()); + var complex = new Transaction() + .from(simpleUtxoWith100000Satoshis) + .to(toAddress, 50000) + .change(changeAddress) + .sign(privateKey); + var cj = complex.toJSON(); + var ctx = new Transaction(cj); + expect(ctx.serialize()).to.equal(complex.serialize()); + }); it('serializes the `change` information', function() { var transaction = new Transaction(); transaction.change(changeAddress); - expect(JSON.parse(transaction.toJSON()).change).to.equal(changeAddress.toString()); + expect(JSON.parse(transaction.toJSON()).changeScript).to.equal(Script.fromAddress(changeAddress).toString()); + expect(new Transaction(transaction.toJSON()).serialize()).to.equal(transaction.serialize()); }); it('serializes correctly p2sh multisig signed tx', function() { var t = new Transaction(tx2hex); @@ -413,11 +434,15 @@ describe('Transaction', function() { var timestamp = 1423504946; var blockHeight = 342734; var date = new Date(timestamp * MILLIS_IN_SECOND); + it('handles a null locktime', function() { + var transaction = new Transaction(); + expect(transaction.getLockTime()).to.equal(null); + }); it('handles a simple example', function() { - var future = new Date(2025,10,30); // Sun Nov 30 2025 + var future = new Date(2025, 10, 30); // Sun Nov 30 2025 var transaction = new Transaction() .lockUntilDate(future); - transaction.nLockTime.should.equal(future.getTime()/1000); + transaction.nLockTime.should.equal(future.getTime() / 1000); transaction.getLockTime().should.deep.equal(future); }); it('accepts a date instance', function() { @@ -430,7 +455,7 @@ describe('Transaction', function() { var transaction = new Transaction() .lockUntilDate(timestamp); transaction.nLockTime.should.equal(timestamp); - transaction.getLockTime().should.deep.equal(new Date(timestamp*1000)); + transaction.getLockTime().should.deep.equal(new Date(timestamp * 1000)); }); it('accepts a block height', function() { var transaction = new Transaction() From 0f17927fde637717274e27e964ee597ce81f3596 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Tue, 10 Feb 2015 18:29:14 -0300 Subject: [PATCH 06/10] fix 0 fee error --- lib/transaction/transaction.js | 2 +- test/transaction/transaction.js | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 8b6a46d..e164559 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -628,7 +628,7 @@ Transaction.prototype.getFee = function() { if (!this._changeScript) { return this._getUnspentValue(); } - return this._fee || this._estimateFee(); + return _.isUndefined(this._fee) ? this._estimateFee() : this._fee; }; /** diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 85282ec..dccd7b0 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -197,11 +197,17 @@ describe('Transaction', function() { .from(simpleUtxoWith100000Satoshis) .to(toAddress, 50000) .change(changeAddress) - .sign(privateKey) + .fee(0) + .sign(privateKey); + + transaction.getChangeOutput().satoshis.should.equal(50000); + + transaction = transaction .to(toAddress, 20000) .sign(privateKey); + transaction.outputs.length.should.equal(3); - transaction.outputs[2].satoshis.should.equal(20000); + transaction.outputs[2].satoshis.should.equal(30000); transaction.outputs[2].script.toString() .should.equal(Script.fromAddress(changeAddress).toString()); }); From eae1638b32ccd1dc64071fed7dc101ab491d5099 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Tue, 10 Feb 2015 18:48:07 -0300 Subject: [PATCH 07/10] add coverage --- test/transaction/transaction.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index dccd7b0..68648e1 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -120,6 +120,8 @@ describe('Transaction', function() { script: Script.buildPublicKeyHashOut(fromAddress).toString(), satoshis: 100000 }; + var weirdUtxoWith100000Satoshis = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis)); + weirdUtxoWith100000Satoshis.script = new Script().add('OP_TRUE'); var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc'; var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up'; var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf'; @@ -488,6 +490,12 @@ describe('Transaction', function() { }).to.throw(errors.Transaction.NLockTimeOutOfRange); }); }); + it('handles weird output', function() { + var transaction = new Transaction() + .from(weirdUtxoWith100000Satoshis) + .to(toAddress, 50000); + should.exist(transaction); + }); }); var tx_empty_hex = '01000000000000000000'; From a7fdcc13a02742a9ac30cbb5ba62eb50f41d11eb Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 11 Feb 2015 10:59:07 -0300 Subject: [PATCH 08/10] weird -> anyone can spend --- test/transaction/transaction.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 68648e1..641cd9c 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -120,8 +120,8 @@ describe('Transaction', function() { script: Script.buildPublicKeyHashOut(fromAddress).toString(), satoshis: 100000 }; - var weirdUtxoWith100000Satoshis = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis)); - weirdUtxoWith100000Satoshis.script = new Script().add('OP_TRUE'); + var anyoneCanSpendUTXO = JSON.parse(JSON.stringify(simpleUtxoWith100000Satoshis)); + anyoneCanSpendUTXO.script = new Script().add('OP_TRUE'); var toAddress = 'mrU9pEmAx26HcbKVrABvgL7AwA5fjNFoDc'; var changeAddress = 'mgBCJAsvzgT2qNNeXsoECg2uPKrUsZ76up'; var changeAddressP2SH = '2N7T3TAetJrSCruQ39aNrJvYLhG1LJosujf'; @@ -490,9 +490,9 @@ describe('Transaction', function() { }).to.throw(errors.Transaction.NLockTimeOutOfRange); }); }); - it('handles weird output', function() { + it('handles anyone-can-spend utxo', function() { var transaction = new Transaction() - .from(weirdUtxoWith100000Satoshis) + .from(anyoneCanSpendUTXO) .to(toAddress, 50000); should.exist(transaction); }); From 8ca396c34f9bc76d6fb744268b6a7382134b8865 Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 11 Feb 2015 11:40:24 -0300 Subject: [PATCH 09/10] refactor transaction input deserializtion --- lib/errors/spec.js | 5 +++- lib/transaction/transaction.js | 45 +++++++++++++++++++++------------ test/transaction/transaction.js | 10 ++++++++ 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/lib/errors/spec.js b/lib/errors/spec.js index eece532..6093724 100644 --- a/lib/errors/spec.js +++ b/lib/errors/spec.js @@ -24,7 +24,7 @@ module.exports = [{ name: 'InvalidArgument', message: function() { return 'Invalid Argument' + (arguments[0] ? (': ' + arguments[0]) : '') + - (arguments[1] ? (' Documentation: ' + docsURL + arguments[1]): ''); + (arguments[1] ? (' Documentation: ' + docsURL + arguments[1]) : ''); } }, { name: 'AbstractMethodInvoked', @@ -53,6 +53,9 @@ module.exports = [{ errors: [{ name: 'MissingScript', message: 'Need a script to create an input' + }, { + name: 'UnsupportedScript', + message: 'Unsupported input script type: {0}' }] }, { name: 'NeedMoreInfo', diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index e164559..eba0bd8 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -226,33 +226,46 @@ Transaction.prototype.toObject = function toObject() { this.outputs.forEach(function(output) { outputs.push(output.toObject()); }); - return { - changeScript: this._changeScript ? this._changeScript.toString() : undefined, - changeIndex: !_.isUndefined(this._changeIndex) ? this._changeIndex : undefined, - fee: this._fee ? this._fee : undefined, + var obj = { version: this.version, inputs: inputs, outputs: outputs, nLockTime: this.nLockTime }; + if (this._changeScript) { + obj.changeScript = this._changeScript.toString(); + } + if (!_.isUndefined(this._changeIndex)) { + obj.changeIndex = this._changeIndex; + } + if (!_.isUndefined(this._fee)) { + obj.fee = this._fee; + } + return obj; }; Transaction.prototype.fromObject = function(transaction) { var self = this; _.each(transaction.inputs, function(input) { - if (input.output && input.output.script) { - input.output.script = new Script(input.output.script); - if (input.output.script.isPublicKeyHashOut()) { - self.addInput(new Input.PublicKeyHash(input)); - return; - } else if (input.output.script.isScriptHashOut() && input.publicKeys && input.threshold) { - self.addInput(new Input.MultiSigScriptHash( - input, input.publicKeys, input.threshold, input.signatures - )); - return; - } + if (!input.output || !input.output.script) { + self.uncheckedAddInput(new Input(input)); + return; } - self.uncheckedAddInput(new Input(input)); + input.output.script = new Script(input.output.script); + var txin; + if (input.output.script.isPublicKeyHashOut()) { + console.log('p2pkh'); + console.log(input.output.script); + txin = new Input.PublicKeyHash(input); + } else if (input.output.script.isScriptHashOut() && input.publicKeys && input.threshold) { + console.log('p2sh'); + txin = new Input.MultiSigScriptHash( + input, input.publicKeys, input.threshold, input.signatures + ); + } else { + throw new errors.Transaction.Input.UnsupportedScript(input.output.script); + } + self.addInput(txin); }); _.each(transaction.outputs, function(output) { self.addOutput(new Output(output)); diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 641cd9c..aab1b22 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -490,12 +490,20 @@ describe('Transaction', function() { }).to.throw(errors.Transaction.NLockTimeOutOfRange); }); }); + it('handles anyone-can-spend utxo', function() { var transaction = new Transaction() .from(anyoneCanSpendUTXO) .to(toAddress, 50000); should.exist(transaction); }); + + it('handles unsupported utxo in tx object', function() { + var transaction = new Transaction(); + transaction.fromJSON.bind(transaction, unsupportedTxObj) + .should.throw('Unsupported input script type: OP_1 OP_ADD OP_2 OP_EQUAL'); + }); + }); var tx_empty_hex = '01000000000000000000'; @@ -506,3 +514,5 @@ var tx_1_id = '779a3e5b3c2c452c85333d8521f804c1a52800e60f4b7c3bbe36f4bab350b72c' var tx2hex = '0100000001e07d8090f4d4e6fcba6a2819e805805517eb19e669e9d2f856b41d4277953d640000000091004730440220248bc60bb309dd0215fbde830b6371e3fdc55685d11daa9a3c43828892e26ce202205f10cd4011f3a43657260a211f6c4d1fa81b6b6bdd6577263ed097cc22f4e5b50147522102fa38420cec94843ba963684b771ba3ca7ce1728dc2c7e7cade0bf298324d6b942103f948a83c20b2e7228ca9f3b71a96c2f079d9c32164cd07f08fbfdb483427d2ee52aeffffffff01180fe200000000001976a914ccee7ce8e8b91ec0bc23e1cfb6324461429e6b0488ac00000000'; + +var unsupportedTxObj = '{"version":1,"inputs":[{"prevTxId":"a477af6b2667c29670467e4e0728b685ee07b240235771862318e29ddbe58458","outputIndex":0,"sequenceNumber":4294967295,"script":"OP_1","output":{"satoshis":1020000,"script":"OP_1 OP_ADD OP_2 OP_EQUAL"}}],"outputs":[{"satoshis":1010000,"script":"OP_DUP OP_HASH160 20 0x7821c0a3768aa9d1a37e16cf76002aef5373f1a8 OP_EQUALVERIFY OP_CHECKSIG"}],"nLockTime":0}'; From 0e1de0403ec3b51ab32020bd1629b9ec3b86c74b Mon Sep 17 00:00:00 2001 From: Manuel Araoz Date: Wed, 11 Feb 2015 12:21:42 -0300 Subject: [PATCH 10/10] remove logs --- lib/transaction/transaction.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index eba0bd8..b657908 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -254,11 +254,9 @@ Transaction.prototype.fromObject = function(transaction) { input.output.script = new Script(input.output.script); var txin; if (input.output.script.isPublicKeyHashOut()) { - console.log('p2pkh'); console.log(input.output.script); txin = new Input.PublicKeyHash(input); } else if (input.output.script.isScriptHashOut() && input.publicKeys && input.threshold) { - console.log('p2sh'); txin = new Input.MultiSigScriptHash( input, input.publicKeys, input.threshold, input.signatures );