Merge pull request #1340 from eordano/bip69

Privacy improvements by sorting inputs and outputs
This commit is contained in:
Braydon Fuller 2015-10-13 17:56:40 -04:00
commit 3d591324c3
5 changed files with 371 additions and 0 deletions

View File

@ -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');
@ -927,6 +928,32 @@ 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.sortInputs(function(inputs) {
var copy = Array.prototype.concat.apply([], inputs);
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(first, second) {
return first.satoshis - second.satoshis
|| compare(first.script.toBuffer(), second.script.toBuffer());
});
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().
@ -951,6 +978,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);

5
npm-shrinkwrap.json generated
View File

@ -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",

View File

@ -68,6 +68,7 @@
"bip21",
"bip32",
"bip37",
"bip69",
"bip70",
"multisig"
],
@ -81,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",

215
test/data/bip69.json Normal file
View File

@ -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]
}
]
}

View File

@ -10,10 +10,13 @@ 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;
var Networks = bitcore.Networks;
var Opcode = bitcore.Opcode;
var errors = bitcore.errors;
var transactionVector = require('../data/tx_creation');
@ -930,8 +933,113 @@ 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');
});
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 */