diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index db2147383..b49488012 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -905,6 +905,41 @@ Transaction.prototype.removeOutput = function(index) { this._updateChangeOutput(); }; +/** + * Sort a transaction's inputs and outputs according to BIP69 + * + * @see {https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki} + * @return {Transaction} this + */ +Transaction.prototype.sort = function() { + this.sortOutputs(function(outputs) { + var copy = Array.prototype.concat.apply([], outputs); + copy.sort(function compare(first, second) { + if (first.satoshis < second.satoshis) { + return -1; + } else if (first.satoshis === second.satoshis) { + return Buffer.compare(first.script.toBuffer(), second.script.toBuffer()); + } else { + return 1; + } + }); + return copy; + }); + this.sortInputs(function(inputs) { + var copy = Array.prototype.concat.apply([], inputs); + copy.sort(function compare(first, second) { + var compareTx = Buffer.compare(first.script.toBuffer(), second.script.toBuffer()); + if (compareTx === 0) { + return first.outputIndex < second.outputIndex ? -1 : 1; + } else { + return compareTx; + } + }); + return copy; + }); + return this; +}; + /** * Randomize this transaction's outputs ordering. The shuffling algorithm is a * version of the Fisher-Yates shuffle, provided by lodash's _.shuffle(). @@ -929,6 +964,20 @@ Transaction.prototype.sortOutputs = function(sortingFunction) { return this._newOutputOrder(outs); }; +/** + * Sort this transaction's inputs, according to a given sorting function that + * takes an array as argument and returns a new array, with the same elements + * but with a different order. + * + * @param {Function} sortingFunction + * @return {Transaction} this + */ +Transaction.prototype.sortInputs = function(sortingFunction) { + this.inputs = sortingFunction(this.inputs); + this._clearSignatures(); + return this; +}; + Transaction.prototype._newOutputOrder = function(newOutputs) { var isInvalidSorting = (this.outputs.length !== newOutputs.length || _.difference(this.outputs, newOutputs).length !== 0); diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 3e8c6d1b3..5f7720ac3 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -14,6 +14,7 @@ var PrivateKey = bitcore.PrivateKey; var Script = bitcore.Script; var Address = bitcore.Address; var Networks = bitcore.Networks; +var Opcode = bitcore.Opcode; var errors = bitcore.errors; var transactionVector = require('../data/tx_creation'); @@ -919,6 +920,64 @@ describe('Transaction', function() { }); }); + + describe('BIP69 Sorting', function() { + + it('sorts inputs correctly', function() { + var from1 = { + txId: '0000000000000000000000000000000000000000000000000000000000000000', + outputIndex: 0, + script: Script.buildPublicKeyHashOut(fromAddress).toString(), + satoshis: 100000 + }; + var from2 = { + txId: '0000000000000000000000000000000000000000000000000000000000000001', + outputIndex: 0, + script: Script.buildPublicKeyHashOut(fromAddress).toString(), + satoshis: 100000 + }; + var from3 = { + txId: '0000000000000000000000000000000000000000000000000000000000000001', + outputIndex: 1, + script: Script.buildPublicKeyHashOut(fromAddress).toString(), + satoshis: 100000 + }; + var tx = new Transaction() + .from(from3) + .from(from2) + .from(from1); + tx.sort(); + tx.inputs[0].prevTxId.toString('hex').should.equal(from1.txId); + tx.inputs[1].prevTxId.toString('hex').should.equal(from2.txId); + tx.inputs[2].prevTxId.toString('hex').should.equal(from3.txId); + tx.inputs[0].outputIndex.should.equal(from1.outputIndex); + tx.inputs[1].outputIndex.should.equal(from2.outputIndex); + tx.inputs[2].outputIndex.should.equal(from3.outputIndex); + }); + + it('sorts outputs correctly', function() { + var tx = new Transaction() + .addOutput(new Transaction.Output({ + script: new Script().add(Opcode(0)), + satoshis: 2 + })) + .addOutput(new Transaction.Output({ + script: new Script().add(Opcode(1)), + satoshis: 2 + })) + .addOutput(new Transaction.Output({ + script: new Script().add(Opcode(0)), + satoshis: 1 + })); + tx.sort(); + tx.outputs[0].satoshis.should.equal(1); + tx.outputs[1].satoshis.should.equal(2); + tx.outputs[2].satoshis.should.equal(2); + tx.outputs[0].script.toString().should.equal('OP_0'); + tx.outputs[1].script.toString().should.equal('OP_0'); + tx.outputs[2].script.toString().should.equal('0x01'); + }); + }); }); var tx_empty_hex = '01000000000000000000';