diff --git a/lib/bloomfilter.js b/lib/bloomfilter.js new file mode 100644 index 0000000..a994774 --- /dev/null +++ b/lib/bloomfilter.js @@ -0,0 +1,47 @@ +'use strict'; + +var bitcore = require('bitcore'); +var BloomFilter = require('bloom-filter'); +var BufferReader = bitcore.encoding.BufferReader; +var BufferWriter = bitcore.encoding.BufferWriter; +var $ = bitcore.util.preconditions; + + +BloomFilter.fromBuffer = function fromBuffer(payload) { + var parser = new BufferReader(payload); + var data = parser.readVarLengthBuffer(); + $.checkState(data.length <= BloomFilter.MAX_BLOOM_FILTER_SIZE, + 'Filter data must be <= MAX_BLOOM_FILTER_SIZE bytes'); + var nHashFuncs = parser.readUInt32LE(); + $.checkState(nHashFuncs <= BloomFilter.MAX_HASH_FUNCS, + 'Filter nHashFuncs must be <= MAX_HASH_FUNCS'); + var nTweak = parser.readUInt32LE(); + var nFlags = parser.readUInt8(); + + var vData = []; + var dataParser = new BufferReader(data); + for(var i = 0; i < data.length; i++) { + vData.push(dataParser.readUInt8()); + } + + return new BloomFilter({ + vData: vData, + nHashFuncs: nHashFuncs, + nTweak: nTweak, + nFlags: nFlags + }); +} + +BloomFilter.prototype.toBuffer = function toBuffer() { + var bw = new BufferWriter(); + bw.writeVarintNum(this.vData.length); + for(var i = 0; i < this.vData.length; i++) { + bw.writeUInt8(this.vData[i]); + } + bw.writeUInt32LE(this.nHashFuncs); + bw.writeUInt32LE(this.nTweak); + bw.writeUInt8(this.nFlags); + return bw.concat(); +}; + +module.exports = BloomFilter; diff --git a/lib/index.js b/lib/index.js index 37dd492..b65689a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -4,5 +4,6 @@ module.exports = { Messages: require('./messages'), Peer: require('./peer'), - Pool: require('./pool') + Pool: require('./pool'), + BloomFilter: require('./bloomfilter') }; diff --git a/lib/messages.js b/lib/messages.js index 2fcf9c0..fe1ff85 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -7,6 +7,7 @@ var Buffers = require('buffers'); var Put = require('bufferput'); var util = require('util'); +var BloomFilter = require('./bloomfilter'); var bitcore = require('bitcore'); var _ = bitcore.deps._; @@ -14,6 +15,7 @@ var _ = bitcore.deps._; var BlockHeaderModel = bitcore.BlockHeader; var BlockModel = bitcore.Block; var BufferReader = bitcore.encoding.BufferReader; +var BufferWriter = bitcore.encoding.BufferWriter; var BufferUtil = bitcore.util.buffer; var $ = bitcore.util.preconditions; var Hash = bitcore.crypto.Hash; @@ -833,6 +835,80 @@ function GetHeaders(starts, stop) { util.inherits(GetHeaders, GetBlocks); module.exports.GetHeaders = Message.COMMANDS.getheaders = GetHeaders; +/** + * Request peer to apply a bloom filter to 'inv' messages sent back + * + * @name P2P.Message.filterload + * @param{BloomFilter} filter - a BloomFilter object + */ +function FilterLoad(filter) { + this.command = 'filterload'; + $.checkArgument(_.isUndefined(filter) || filter instanceof BloomFilter, + 'BloomFilter object or undefined required for FilterLoad'); + this.filter = filter; + return this; +} +util.inherits(FilterLoad, Message); + +FilterLoad.prototype.fromBuffer = function(payload) { + this.filter = BloomFilter.fromBuffer(payload); + return this; +}; + +FilterLoad.prototype.getPayload = function() { + if(this.filter) { + return this.filter.toBuffer() + } else { + return BufferUtil.EMPTY_BUFFER; + } +}; + +module.exports.FilterLoad = Message.COMMANDS.filterload = FilterLoad; + +/** + * Request peer to add data to a bloom filter already set by 'filterload' + * + * @name P2P.Message.filteradd + * @param{Buffer} data - Array of bytes representing bloom filter data + */ +function FilterAdd(data) { + this.command = 'filteradd'; + this.data = data || new Buffer(0,'hex'); + return this; +} +util.inherits(FilterAdd, Message); + +FilterAdd.prototype.fromBuffer = function(payload) { + $.checkArgument(payload); + var parser = new BufferReader(payload); + this.data = parser.readVarLengthBuffer(); + $.checkState(this.data.length <= BloomFilter.MAX_BLOOM_FILTER_SIZE, + 'FilterAdd data must be <= 520 bytes'); + this._checkFinished(parser); + return this; +}; + +FilterAdd.prototype.getPayload = function() { + var bw = new BufferWriter(); + bw.writeVarintNum(this.data.length); + bw.write(this.data); + return bw.concat(); +}; + +module.exports.FilterAdd = Message.COMMANDS.filterload = FilterAdd; + +/** + * Request peer to apply a bloom filter to 'inv' messages sent back + * + * @name P2P.Message.filterclear + */ +function FilterClear() { + this.command = 'filterclear'; +} +util.inherits(FilterClear, Message); + +module.exports.FilterClear = Message.COMMANDS.filterclear = FilterClear; + /** * Request for transactions on the mempool * diff --git a/lib/pool.js b/lib/pool.js index 4f36e44..98f20ee 100644 --- a/lib/pool.js +++ b/lib/pool.js @@ -106,7 +106,8 @@ Pool.MaxConnectedPeers = 8; Pool.RetrySeconds = 30; Pool.PeerEvents = ['version', 'inv', 'getdata', 'ping', 'pong', 'addr', 'getaddr', 'verack', 'reject', 'alert', 'headers', 'block', - 'tx', 'getblocks', 'getheaders', 'error' + 'tx', 'getblocks', 'getheaders', 'error', 'filterload', 'filteradd', + 'filterclear' ]; diff --git a/package.json b/package.json index 0e65ac3..a344506 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "dependencies": { "bitcore": "^0.10.2", + "bloom-filter": "^0.1.1", "bufferput": "^0.1.2", "buffers": "^0.1.1", "socks5-client": "^0.3.6" diff --git a/test/bloomfilter.js b/test/bloomfilter.js new file mode 100644 index 0000000..e7859f0 --- /dev/null +++ b/test/bloomfilter.js @@ -0,0 +1,77 @@ +'use strict'; + +var chai = require('chai'); + +var assert = require('assert'); +var bitcore = require('bitcore'); +var Data = require('./data/messages'); +var P2P = require('../'); +var BloomFilter = P2P.BloomFilter; + +// convert a hex string to a bytes buffer +function ParseHex(str) { + var result = []; + while (str.length >= 2) { + result.push(parseInt(str.substring(0, 2), 16)); + str = str.substring(2, str.length); + } + var buf = new Buffer(result, 16); + return buf; +} + +describe('BloomFilter', function() { + + it('BloomFilter#fromBuffer and toBuffer methods work', function() { + var testPayload = Data.FILTERLOAD.payload; + var filter = new BloomFilter.fromBuffer(new Buffer(testPayload, 'hex')); + filter.toBuffer().toString('hex').should.equal(testPayload); + }); + + // test data from: https://github.com/bitcoin/bitcoin/blob/master/src/test/bloom_tests.cpp + + it('correctly serialize filter with public keys added', function() { + + var privateKey = bitcore.PrivateKey.fromWIF('5Kg1gnAjaLfKiwhhPpGS3QfRg2m6awQvaj98JCZBZQ5SuS2F15C'); + var publicKey = privateKey.toPublicKey(); + + var filter = BloomFilter.create(2, 0.001, 0, BloomFilter.BLOOM_UPDATE_ALL); + filter.insert(publicKey.toBuffer()); + filter.insert(bitcore.crypto.Hash.sha256ripemd160(publicKey.toBuffer())); + + var expectedFilter = BloomFilter.fromBuffer(ParseHex('038fc16b080000000000000001')); + + filter.toBuffer().should.deep.equal(expectedFilter.toBuffer()); + + }); + + it('correctly serialize to a buffer', function() { + + var filter = BloomFilter.create(3, 0.01, 0, BloomFilter.BLOOM_UPDATE_ALL); + + filter.insert(ParseHex('99108ad8ed9bb6274d3980bab5a85c048f0950c8')); + assert(filter.contains(ParseHex('99108ad8ed9bb6274d3980bab5a85c048f0950c8'))); + // one bit different in first byte + assert(!filter.contains(ParseHex('19108ad8ed9bb6274d3980bab5a85c048f0950c8'))); + filter.insert(ParseHex('b5a2c786d9ef4658287ced5914b37a1b4aa32eee')); + assert(filter.contains(ParseHex("b5a2c786d9ef4658287ced5914b37a1b4aa32eee"))); + filter.insert(ParseHex('b9300670b4c5366e95b2699e8b18bc75e5f729c5')); + assert(filter.contains(ParseHex('b9300670b4c5366e95b2699e8b18bc75e5f729c5'))); + + var actual = filter.toBuffer(); + var expected = new Buffer('03614e9b050000000000000001', 'hex'); + + actual.should.deep.equal(expected); + }); + + it('correctly deserialize a buffer', function() { + + var buffer = new Buffer('03614e9b050000000000000001', 'hex'); + var filter = BloomFilter.fromBuffer(buffer); + + assert(filter.contains(ParseHex('99108ad8ed9bb6274d3980bab5a85c048f0950c8'))); + assert(!filter.contains(ParseHex('19108ad8ed9bb6274d3980bab5a85c048f0950c8'))); + assert(filter.contains(ParseHex("b5a2c786d9ef4658287ced5914b37a1b4aa32eee"))); + assert(filter.contains(ParseHex('b9300670b4c5366e95b2699e8b18bc75e5f729c5'))); + }); + +}); diff --git a/test/data/messages.json b/test/data/messages.json index 8bbb310..551b578 100644 --- a/test/data/messages.json +++ b/test/data/messages.json @@ -65,5 +65,17 @@ "PONG": { "message": "f9beb4d9706f6e67000000000000000008000000c6466f1e6b86480ae969867c", "payload": "6b86480ae969867c" + }, + "FILTERLOAD": { + "message": "f9beb4d966696c7465726c6f61640000210000002ef97a71170000000000000000000000000000000000000000000000060000000000000000", + "payload": "170000000000000000000000000000000000000000000000060000000000000000" + }, + "FILTERADD": { + "message": "f9beb4d966696c7465726c6f61640000150000009727ea0a1499108ad8ed9bb6274d3980bab5a85c048f0950c8", + "payload": "1499108ad8ed9bb6274d3980bab5a85c048f0950c8" + }, + "FILTERCLEAR": { + "message": "f9beb4d966696c7465726c6365617200000000005df6e0e2", + "payload": "" } } diff --git a/test/messages.js b/test/messages.js index ee260c3..6cd29d6 100644 --- a/test/messages.js +++ b/test/messages.js @@ -8,6 +8,7 @@ var Buffers = require('buffers'); var bitcore = require('bitcore'); var Data = require('./data/messages'); var P2P = require('../'); +var BloomFilter = P2P.BloomFilter; var Messages = P2P.Messages; var Networks = bitcore.Networks; var BufferUtils = bitcore.util.buffer; @@ -26,6 +27,9 @@ describe('Messages', function() { Alert: 'alert', Reject: 'reject', Block: 'block', + FilterLoad: 'filterload', + FilterAdd: 'filteradd', + FilterClear: 'filterclear', GetBlocks: 'getblocks', GetHeaders: 'getheaders', GetData: 'getdata', @@ -34,7 +38,7 @@ describe('Messages', function() { Transaction: 'tx', NotFound: 'notfound' }; - // TODO: add data for these + // TODO: add data for these var noPayload = ['Reject', 'GetBlocks', 'GetHeaders']; var names = Object.keys(commands); describe('named', function() { @@ -131,4 +135,10 @@ describe('Messages', function() { }); }); + it('FilterLoad#fromBuffer method works', function() { + var testPayload = Data.FILTERLOAD.payload; + var msg = new Messages.FilterLoad().fromBuffer(new Buffer(testPayload, 'hex')); + msg.getPayload().toString('hex').should.equal(testPayload); + }); + });