From fb65145ba7666166b786f1549c9fb70ed37158fc Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Tue, 22 Sep 2015 16:07:12 -0700 Subject: [PATCH 1/4] Privacy improvement by sorting inputs and outputs See BIP69 for more details: https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki --- lib/transaction/transaction.js | 49 +++++++++++++++++++++++++++ test/transaction/transaction.js | 59 +++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index db21473..b494880 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 3e8c6d1..5f7720a 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'; From 026f878e51632a36da288a289e24b8dd1c366128 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 30 Sep 2015 10:07:02 -0700 Subject: [PATCH 2/4] Add tests for bip69 from bitcoinjs --- lib/transaction/transaction.js | 29 ++--- lib/util/buffer.js | 43 +++++++ package.json | 1 + test/data/bip69.json | 215 ++++++++++++++++++++++++++++++++ test/transaction/transaction.js | 49 ++++++++ 5 files changed, 318 insertions(+), 19 deletions(-) create mode 100644 test/data/bip69.json diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index b494880..5138d7b 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -912,28 +912,19 @@ Transaction.prototype.removeOutput = function(index) { * @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 BufferUtil.compare(first.prevTxId, second.prevTxId) + || first.outputIndex - second.outputIndex; + }); + return copy; + }); + this.sortOutputs(function(outputs) { + var copy = Array.prototype.concat.apply([], outputs); + copy.sort(function compare(first, second) { + return first.satoshis - second.satoshis + || BufferUtil.compare(first.script.toBuffer(), second.script.toBuffer()); }); return copy; }); diff --git a/lib/util/buffer.js b/lib/util/buffer.js index c78b939..0cfe5af 100644 --- a/lib/util/buffer.js +++ b/lib/util/buffer.js @@ -170,6 +170,49 @@ module.exports = { hexToBuffer: function hexToBuffer(string) { assert(js.isHexa(string)); return new buffer.Buffer(string, 'hex'); + }, + + /** + * Browser shim for buffer compare + * + * MIT. Copyright by Feross Aboukhadijeh, and other contributors. + * Originally forked from an MIT-licensed module by Romain Beauxis. + * @see {https://github.com/feross/buffer/blob/bc33ea4618e6846148e35aee78dfc28d999bdd1c/index.js#L264} + */ + compare: function(a, b) { + if (Buffer.compare) { + return Buffer.compare(a, b); + } else { + if (a === b) { + return 0; + } + + var x = a.length; + var y = b.length; + + var i = 0; + var len = Math.min(x, y); + while (i < len) { + if (a[i] !== b[i]) { + break; + } + + ++i; + } + + if (i !== len) { + x = a[i]; + y = b[i]; + } + + if (x < y) { + return -1; + } + if (y < x) { + return 1; + } + return 0; + } } }; diff --git a/package.json b/package.json index 736a02c..f474151 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "bip21", "bip32", "bip37", + "bip39", "bip70", "multisig" ], diff --git a/test/data/bip69.json b/test/data/bip69.json new file mode 100644 index 0000000..7a46d81 --- /dev/null +++ b/test/data/bip69.json @@ -0,0 +1,215 @@ +{ + "inputs": [ + { + "description": "Ordered by txId, descending (reverse-byte-order ascending)", + "inputs": [ + { + "txId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "vout": 0 + }, + { + "txId": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "vout": 0 + }, + { + "txId": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "vout": 0 + }, + { + "txId": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbff", + "vout": 0 + }, + { + "txId": "ffbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "vout": 0 + } + ], + "expected": [0, 2, 3, 1, 4] + }, + { + "description": "Ordered by vout, ascending", + "inputs": [ + { + "txId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "vout": 1 + }, + { + "txId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "vout": 2 + }, + { + "txId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "vout": 0 + } + ], + "expected": [2, 0, 1] + }, + { + "description": "Ordered by txId, then vout", + "inputs": [ + { + "txId": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "vout": 99 + }, + { + "txId": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "vout": 99 + }, + { + "txId": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "vout": 0 + }, + { + "txId": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "vout": 0 + } + ], + "expected": [0, 3, 1, 2] + }, + { + "description": "BIP69 test vector 1", + "inputs": [ + { "txId": "0e53ec5dfb2cb8a71fec32dc9a634a35b7e24799295ddd5278217822e0b31f57", "vout": 0 }, + { "txId": "26aa6e6d8b9e49bb0630aac301db6757c02e3619feb4ee0eea81eb1672947024", "vout": 1 }, + { "txId": "28e0fdd185542f2c6ea19030b0796051e7772b6026dd5ddccd7a2f93b73e6fc2", "vout": 0 }, + { "txId": "381de9b9ae1a94d9c17f6a08ef9d341a5ce29e2e60c36a52d333ff6203e58d5d", "vout": 1 }, + { "txId": "3b8b2f8efceb60ba78ca8bba206a137f14cb5ea4035e761ee204302d46b98de2", "vout": 0 }, + { "txId": "402b2c02411720bf409eff60d05adad684f135838962823f3614cc657dd7bc0a", "vout": 1 }, + { "txId": "54ffff182965ed0957dba1239c27164ace5a73c9b62a660c74b7b7f15ff61e7a", "vout": 1 }, + { "txId": "643e5f4e66373a57251fb173151e838ccd27d279aca882997e005016bb53d5aa", "vout": 0 }, + { "txId": "6c1d56f31b2de4bfc6aaea28396b333102b1f600da9c6d6149e96ca43f1102b1", "vout": 1 }, + { "txId": "7a1de137cbafb5c70405455c49c5104ca3057a1f1243e6563bb9245c9c88c191", "vout": 0 }, + { "txId": "7d037ceb2ee0dc03e82f17be7935d238b35d1deabf953a892a4507bfbeeb3ba4", "vout": 1 }, + { "txId": "a5e899dddb28776ea9ddac0a502316d53a4a3fca607c72f66c470e0412e34086", "vout": 0 }, + { "txId": "b4112b8f900a7ca0c8b0e7c4dfad35c6be5f6be46b3458974988e1cdb2fa61b8", "vout": 0 }, + { "txId": "bafd65e3c7f3f9fdfdc1ddb026131b278c3be1af90a4a6ffa78c4658f9ec0c85", "vout": 0 }, + { "txId": "de0411a1e97484a2804ff1dbde260ac19de841bebad1880c782941aca883b4e9", "vout": 1 }, + { "txId": "f0a130a84912d03c1d284974f563c5949ac13f8342b8112edff52971599e6a45", "vout": 0 }, + { "txId": "f320832a9d2e2452af63154bc687493484a0e7745ebd3aaf9ca19eb80834ad60", "vout": 0 } + ], + "expected": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] + }, + { + "description": "BIP69 test vector 2", + "inputs": [ + { "txId": "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055", "vout": 0 }, + { "txId": "35288d269cee1941eaebb2ea85e32b42cdb2b04284a56d8b14dcc3f5c65d6055", "vout": 1 } + ], + "expected": [0, 1] + } + ], + "outputs": [ + { + "description": "Ordered by Amount, ascending", + "outputs": [ + { + "script": "00000000", + "value": 3000 + }, + { + "script": "00000000", + "value": 2000 + }, + { + "script": "00000000", + "value": 1000 + } + ], + "expected": [2, 1, 0] + }, + { + "description": "Ordered by Script, ascending", + "outputs": [ + { + "script": "00000000", + "value": 1000 + }, + { + "script": "22222222", + "value": 1000 + }, + { + "script": "11111111", + "value": 1000 + } + ], + "expected": [0, 2, 1] + }, + { + "description": "Ordered by Amount, then Script", + "outputs": [ + { + "script": "11111111", + "value": 1000 + }, + { + "script": "11111111", + "value": 2000 + }, + { + "script": "00000000", + "value": 3000 + }, + { + "script": "00000000", + "value": 2000 + } + ], + "expected": [0, 3, 1, 2] + }, + { + "description": "Sorting is irrelevant for equivalent outputs", + "outputs": [ + { + "script": "00000000", + "value": 2000 + }, + { + "script": "11111111", + "value": 2000 + }, + { + "script": "00000000", + "value": 2000 + }, + { + "script": "11111111", + "value": 3000 + }, + { + "script": "22222222", + "value": 3000 + } + ], + "expected": [0, 2, 1, 3, 4] + }, + { + "description": "BIP69 test vector 1", + "outputs": [ + { + "script": "76a9144a5fba237213a062f6f57978f796390bdcf8d01588ac", + "value": 400057456 + }, + { + "script": "76a9145be32612930b8323add2212a4ec03c1562084f8488ac", + "value": 40000000000 + } + ], + "expected": [0, 1] + }, + { + "description": "BIP69 test vector 2", + "outputs": [ + { + "script": "41046a0765b5865641ce08dd39690aade26dfbf5511430ca428a3089261361cef170e3929a68aee3d8d4848b0c5111b0a37b82b86ad559fd2a745b44d8e8d9dfdc0cac", + "value": 100000000 + }, + { + "script": "41044a656f065871a353f216ca26cef8dde2f03e8c16202d2e8ad769f02032cb86a5eb5e56842e92e19141d60a01928f8dd2c875a390f67c1f6c94cfc617c0ea45afac", + "value": 2400000000 + } + ], + "expected": [0, 1] + } + ] +} diff --git a/test/transaction/transaction.js b/test/transaction/transaction.js index 5f7720a..598d9eb 100644 --- a/test/transaction/transaction.js +++ b/test/transaction/transaction.js @@ -10,6 +10,8 @@ var sinon = require('sinon'); var bitcore = require('../..'); var BN = bitcore.crypto.BN; var Transaction = bitcore.Transaction; +var Input = bitcore.Transaction.Input; +var Output = bitcore.Transaction.Output; var PrivateKey = bitcore.PrivateKey; var Script = bitcore.Script; var Address = bitcore.Address; @@ -977,9 +979,56 @@ describe('Transaction', function() { tx.outputs[1].script.toString().should.equal('OP_0'); tx.outputs[2].script.toString().should.equal('0x01'); }); + + describe('bitcoinjs fixtures', function() { + + var fixture = require('../data/bip69.json'); + + // returns index-based order of sorted against original + var getIndexOrder = function(original, sorted) { + return sorted.map(function (value) { + return original.indexOf(value); + }); + }; + + fixture.inputs.forEach(function(inputSet) { + it(inputSet.description, function() { + var tx = new Transaction(); + inputSet.inputs = inputSet.inputs.map(function(input) { + var input = new Input({ + prevTxId: input.txId, + outputIndex: input.vout, + script: new Script(), + output: new Output({ script: new Script(), satoshis: 0 }) + }); + input.clearSignatures = function () {}; + return input; + }); + tx.inputs = inputSet.inputs; + tx.sort(); + getIndexOrder(inputSet.inputs, tx.inputs).should.deep.equal(inputSet.expected); + }); + }); + fixture.outputs.forEach(function(outputSet) { + it(outputSet.description, function() { + var tx = new Transaction(); + outputSet.outputs = outputSet.outputs.map(function(output) { + return new Output({ + script: new Script(output.script), + satoshis: output.value + }); + }); + tx.outputs = outputSet.outputs; + tx.sort(); + getIndexOrder(outputSet.outputs, tx.outputs).should.deep.equal(outputSet.expected); + }); + }); + + }); }); }); + var tx_empty_hex = '01000000000000000000'; /* jshint maxlen: 1000 */ From 20cc98df57eb76fdf24fbcecd3a9ab0cfbd0e4d2 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Wed, 30 Sep 2015 18:46:24 -0700 Subject: [PATCH 3/4] Fix typo --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f474151..23eee23 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "bip21", "bip32", "bip37", - "bip39", + "bip69", "bip70", "multisig" ], From 19f3fe0de3c4693fc0fed2b935c39ae008b44510 Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Thu, 1 Oct 2015 12:38:04 -0700 Subject: [PATCH 4/4] Using buffer-compare instead of copy&paste --- lib/transaction/transaction.js | 9 +++---- lib/util/buffer.js | 43 ---------------------------------- npm-shrinkwrap.json | 5 ++++ package.json | 1 + 4 files changed, 11 insertions(+), 47 deletions(-) diff --git a/lib/transaction/transaction.js b/lib/transaction/transaction.js index 5138d7b..f6611f6 100644 --- a/lib/transaction/transaction.js +++ b/lib/transaction/transaction.js @@ -3,6 +3,7 @@ var _ = require('lodash'); var $ = require('../util/preconditions'); var buffer = require('buffer'); +var compare = Buffer.compare || require('buffer-compare'); var errors = require('../errors'); var BufferUtil = require('../util/buffer'); @@ -914,17 +915,17 @@ Transaction.prototype.removeOutput = function(index) { Transaction.prototype.sort = function() { this.sortInputs(function(inputs) { var copy = Array.prototype.concat.apply([], inputs); - copy.sort(function compare(first, second) { - return BufferUtil.compare(first.prevTxId, second.prevTxId) + copy.sort(function(first, second) { + return compare(first.prevTxId, second.prevTxId) || first.outputIndex - second.outputIndex; }); return copy; }); this.sortOutputs(function(outputs) { var copy = Array.prototype.concat.apply([], outputs); - copy.sort(function compare(first, second) { + copy.sort(function(first, second) { return first.satoshis - second.satoshis - || BufferUtil.compare(first.script.toBuffer(), second.script.toBuffer()); + || compare(first.script.toBuffer(), second.script.toBuffer()); }); return copy; }); diff --git a/lib/util/buffer.js b/lib/util/buffer.js index 0cfe5af..c78b939 100644 --- a/lib/util/buffer.js +++ b/lib/util/buffer.js @@ -170,49 +170,6 @@ module.exports = { hexToBuffer: function hexToBuffer(string) { assert(js.isHexa(string)); return new buffer.Buffer(string, 'hex'); - }, - - /** - * Browser shim for buffer compare - * - * MIT. Copyright by Feross Aboukhadijeh, and other contributors. - * Originally forked from an MIT-licensed module by Romain Beauxis. - * @see {https://github.com/feross/buffer/blob/bc33ea4618e6846148e35aee78dfc28d999bdd1c/index.js#L264} - */ - compare: function(a, b) { - if (Buffer.compare) { - return Buffer.compare(a, b); - } else { - if (a === b) { - return 0; - } - - var x = a.length; - var y = b.length; - - var i = 0; - var len = Math.min(x, y); - while (i < len) { - if (a[i] !== b[i]) { - break; - } - - ++i; - } - - if (i !== len) { - x = a[i]; - y = b[i]; - } - - if (x < y) { - return -1; - } - if (y < x) { - return 1; - } - return 0; - } } }; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index e059d84..dceb306 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -12,6 +12,11 @@ "from": "bs58@=2.0.0", "resolved": "https://registry.npmjs.org/bs58/-/bs58-2.0.0.tgz" }, + "buffer-compare": { + "version": "1.0.0", + "from": "buffer-compare@=1.0.0", + "resolved": "https://registry.npmjs.org/buffer-compare/-/buffer-compare-1.0.0.tgz" + }, "elliptic": { "version": "3.0.3", "from": "elliptic@=3.0.3", diff --git a/package.json b/package.json index 23eee23..4a7c967 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "dependencies": { "bn.js": "=2.0.4", "bs58": "=2.0.0", + "buffer-compare": "=1.0.0", "elliptic": "=3.0.3", "hash.js": "=1.0.2", "inherits": "=2.0.1",