Comparing version 0.1.1 to 0.2.0
@@ -36,2 +36,9 @@ var BTCNetwork = require('../lib/BTCNetwork').BTCNetwork; | ||
console.log('Peer '+d.peer.getUUID()+' knows of Transaction '+d.hash.toString('hex')); | ||
n.getData({type:1, hash:d.hash}, d.peer, function(err, rs) { | ||
if (err !== false) { | ||
console.log('Data returned error: '+err); | ||
return; | ||
} | ||
console.log('Data returned:', rs); | ||
}); | ||
}); | ||
@@ -38,0 +45,0 @@ n.on('transactionBlock', function transactionInv(d) { |
var PeerManager = require('p2p-manager').PeerManager; | ||
var Message = require('./Message').Message; | ||
var Struct = require('./Struct').Struct | ||
var sha256 = require('sha256'); | ||
@@ -43,2 +45,5 @@ var crypto = require('crypto'); | ||
this.pendingGetData = {}; | ||
this.getDataDebounce = false; | ||
var self = this; | ||
@@ -51,5 +56,12 @@ this.manager | ||
var cmd = d.command; | ||
var handlerName = 'parse' + cmd.charAt(0).toUpperCase() + cmd.slice(1) + 'Message'; | ||
if (typeof self[handlerName] == 'function') { // does 'parseCommandMessage' exist as a method? | ||
var rs = self[handlerName].call(self, d.data, d.peer); | ||
var parserName = 'parse' + cmd.charAt(0).toUpperCase() + cmd.slice(1) + 'Message'; | ||
if (typeof self[parserName] == 'function') { // does 'parseCommandMessage' exist as a method? | ||
var rs = self[parserName].call(self, d.data); | ||
if (rs === false) return; // Invalid data was sent; ignore it | ||
var handlerName = 'handle' + cmd.charAt(0).toUpperCase() + cmd.slice(1) + 'Message'; | ||
if (typeof self[handlerName] == 'function') { | ||
if (self[handlerName].call(self, rs, d.peer) === false) return; | ||
} | ||
self.emit('message', { | ||
@@ -133,15 +145,107 @@ peer: d.peer, | ||
BTCNetwork.prototype.parseAddrMessage = function parseAddrMessage(data, peer) { | ||
BTCNetwork.prototype.parseAddrMessage = function parseAddrMessage(data) { | ||
var addrs = []; | ||
var addrNum = Message.prototype.getVarInt(data, 0); | ||
for (var i = addrNum[1]; i < data.length; i += 30) { | ||
addrs.push(this.getAddr(data.slice(i, i+30))); | ||
var raw = new Struct(data); | ||
try { | ||
var addrNum = raw.readVarInt(); | ||
for (var i = 0; i < addrNum; i++) { | ||
addrs.push(this.getAddr(raw.raw(30))); | ||
} | ||
} catch (e) { | ||
return false; | ||
} | ||
if (addrs.length != addrNum[0]) { | ||
this._error('Was supposed to get '+addrNum[0]+' addresses, but got '+parsed.length+' instead', 'info'); | ||
} | ||
this.manager.addPool(addrs); // Add these peers to the list of possible peers to connect to | ||
return { addrs: addrs }; | ||
}; | ||
BTCNetwork.prototype.handleAddrMessage = function handleAddrMessage(data, peer) { | ||
this.manager.addPool(data.addrs); // Add these peers to the list of possible peers to connect to | ||
}; | ||
BTCNetwork.prototype.parseAlertMessage = function parseAlertMessage(data) { | ||
var parsed = {}; | ||
var raw = new Struct(data); | ||
try { | ||
var msgSize = raw.readVarInt(); | ||
var message = raw.raw(msgSize); | ||
var sigSize = raw.readVarInt(); | ||
parsed.signature = raw.raw(sigSize); | ||
message = new Struct(message); | ||
parsed.version = message.readUInt32LE(); | ||
parsed.relay_until = message.raw(8); | ||
if (parsed.relay_until.readUInt32LE(4) == 0) { | ||
parsed.relay_until = new Date(parsed.relay_until.readUInt32LE(0)*1000); | ||
} | ||
parsed.expiration = message.raw(8); | ||
if (parsed.expiration.readUInt32LE(4) == 0) { | ||
parsed.expiration = new Date(parsed.expiration.readUInt32LE(0)*1000); | ||
} | ||
parsed.uuid = message.readUInt32LE(); | ||
parsed.cancel = message.readUInt32LE(); | ||
var num = message.readVarInt(); | ||
parsed.cancel_set = []; | ||
for (var i = 0; i < num; i++) { | ||
parsed.cancel_set.push(message.readUInt32LE()); | ||
} | ||
parsed.min_version = message.readUInt32LE(); | ||
parsed.max_version = message.readUInt32LE(); | ||
num = message.readVarInt(); | ||
parsed.subversion_set = []; | ||
for (var i = 0; i < num; i++) { | ||
parsed.subversion_set.push(message.readVarString()); | ||
} | ||
parsed.priority = message.readUInt32LE(); | ||
parsed.comment = message.readVarString(); | ||
parsed.status_bar = message.readVarString(); | ||
num = message.readVarInt(); | ||
parsed.reserved = message.raw(num); | ||
} catch(e) { | ||
return false; | ||
} | ||
return parsed; | ||
}; | ||
BTCNetwork.prototype.parseGetblocksMessage = function parseGetblocksMessage(data) { | ||
var parsed = {}; | ||
var raw = new Struct(data); | ||
try { | ||
parsed.version = raw.readUInt32LE(); | ||
var hashCount = raw.readVarInt(); | ||
parsed.hashes = []; | ||
for (var i = 0; i < hashCount; i++) { | ||
parsed.hashes.push(raw.raw(32)); | ||
} | ||
parsed.hash_stop = raw.raw(32); | ||
} catch (e) { | ||
return false; | ||
} | ||
return parsed; | ||
}; | ||
BTCNetwork.prototype.parseGetheadersMessage = function parseGetHeadersMessage(data) { | ||
return this.parseGetblocksMessage(data); | ||
}; | ||
BTCNetwork.prototype.parseHeadersMessage = function parseHeadersMessage(data) { | ||
var headers = []; | ||
var raw = new Struct(data); | ||
try { | ||
var num = raw.readVarInt(); | ||
for (var i = 0; i < num; i++) { | ||
var header = {}; | ||
header.version = raw.readUInt32LE(); | ||
header.prev_block = raw.raw(32); | ||
header.merkle_root = raw.raw(32); | ||
header.timestamp = new Date(raw.readUInt32LE()*1000); | ||
header.difficulty = raw.readUInt32LE(); | ||
header.nonce = raw.raw(4); | ||
header.txn_count = raw.readVarInt(); | ||
headers.push(header); | ||
} | ||
} catch (e) { | ||
return false; | ||
} | ||
return { headers: headers }; | ||
}; | ||
BTCNetwork.prototype.invTypes = { | ||
@@ -152,44 +256,113 @@ '0': 'error', | ||
}; | ||
BTCNetwork.prototype.parseInvMessage = function parseInvMessage(data, peer) { | ||
BTCNetwork.prototype.parseInvMessage = function parseInvMessage(data) { | ||
var items = []; | ||
var invNum = Message.prototype.getVarInt(data, 0); | ||
for (var i = invNum[1]; i < data.length; i += 36) { | ||
var inv_vect = {}; | ||
var tmp = data.slice(i, i+36); | ||
inv_vect.type = tmp.readUInt32LE(0); | ||
inv_vect.type_name = (typeof this.invTypes[inv_vect.type] != 'undefined')? this.invTypes[inv_vect.type] : 'unknown'; | ||
inv_vect.hash = new Buffer(32); | ||
tmp.copy(inv_vect.hash, 0, 4); | ||
items.push(inv_vect); | ||
this.emit(inv_vect.type_name+'Inv', { | ||
var raw = new Struct(data); | ||
try { | ||
var invNum = raw.readVarInt(); | ||
for (var i = 0; i < invNum; i++) { | ||
var inv_vect = {}; | ||
inv_vect.type = raw.readUInt32LE(); | ||
inv_vect.type_name = (typeof this.invTypes[inv_vect.type] != 'undefined')? this.invTypes[inv_vect.type] : 'unknown'; | ||
inv_vect.hash = raw.raw(32); | ||
items.push(inv_vect); | ||
} | ||
} catch(e) { | ||
return false; | ||
} | ||
return { items: items } | ||
}; | ||
BTCNetwork.prototype.handleInvMessage = function handleInvMessage(data, peer) { | ||
for (var i = 0; i < data.items.length; i++) { | ||
this.emit(data.items[i].type_name+'Inv', { | ||
peer: peer, | ||
hash: inv_vect.hash | ||
hash: data.items[i].hash | ||
}); | ||
} | ||
if (items.length != invNum[0]) { | ||
this._error('Was supposted to get '+invNum[0]+' inventory items, but got '+items.length+' instead', 'info'); | ||
}; | ||
BTCNetwork.prototype.parseNotfoundMessage = function parseNotfoundMessage(data) { | ||
return this.parseInvMessage(data); | ||
}; | ||
BTCNetwork.prototype.parsePingMessage = function parsePingMessage(data) { | ||
var parsed = {}; | ||
var raw = new Struct(data); | ||
try { | ||
parsed.nonce = raw.raw(8); | ||
} catch(e) { | ||
return false; | ||
} | ||
return { items: items }; | ||
return parsed; | ||
}; | ||
BTCNetwork.prototype.parsePongMessage = function parsePongMessage(data) { | ||
return this.parsePingMessage(data); | ||
}; | ||
BTCNetwork.prototype.parseTxMessage = function parseTxMessage(data) { | ||
var parsed = {}; | ||
var raw = new Struct(data); | ||
try { | ||
parsed.version = raw.readUInt32LE(); | ||
var inCount = raw.readVarInt(); | ||
parsed.txIn = []; | ||
for (var i = 0; i < inCount; i++) { | ||
var txIn = { outPoint: {} }; | ||
txIn.outPoint.hash = raw.raw(32); | ||
txIn.outPoint.index = raw.readUInt32LE(); | ||
var scriptSize = raw.readVarInt(); | ||
txIn.signature = raw.raw(scriptSize); | ||
txIn.sequence = raw.readUInt32LE(); | ||
parsed.txIn.push(txIn); | ||
} | ||
var outCount = raw.readVarInt(); | ||
parsed.txOut = []; | ||
for (i = 0; i < outCount; i++) { | ||
var txOut = {}; | ||
txOut.value = raw.raw(8); | ||
if (txOut.value.readUInt32LE(4) == 0) { | ||
// Actually a 32-bit number | ||
txOut.value = txOut.value.readUInt32LE(0); | ||
} | ||
var scriptSize = raw.readVarInt(); | ||
txOut.script = raw.raw(scriptSize); | ||
parsed.txOut.push(txOut); | ||
} | ||
parsed.lock = raw.readUInt32LE(); | ||
parsed.raw = new Buffer(data.length); | ||
data.copy(parsed.raw); | ||
parsed.hash = new Buffer(sha256.x2(data, { asBytes: true })); | ||
} catch(e) { | ||
return false; | ||
} | ||
return parsed; | ||
}; | ||
BTCNetwork.prototype.parseVersionMessage = function parseVersionMessage(data, peer) { | ||
var parsed = {}; | ||
parsed.version = data.readUInt32LE(0); | ||
parsed.services = new Buffer(8); | ||
data.copy(parsed.services, 0, 4, 12); | ||
parsed.time = new Buffer(8); | ||
data.copy(parsed.time, 0, 12, 20); | ||
if (parsed.time.readUInt32LE(4) == 0) { | ||
// 32-bit date; no need to keep as buffer | ||
parsed.time = new Date(parsed.time.readUInt32LE(0)*1000); | ||
var raw = new Struct(data); | ||
try { | ||
parsed.version = raw.readUInt32LE(0); | ||
parsed.services = raw.raw(8); | ||
parsed.time = raw.raw(8); | ||
if (parsed.time.readUInt32LE(4) == 0) { | ||
// 32-bit date; no need to keep as buffer | ||
parsed.time = new Date(parsed.time.readUInt32LE(0)*1000); | ||
} | ||
parsed.addr_recv = this.getAddr(raw.raw(26)); | ||
parsed.addr_from = this.getAddr(raw.raw(26)); | ||
parsed.nonce = raw.raw(8); | ||
parsed.client = raw.readVarString(); | ||
parsed.height = raw.readUInt32LE(); | ||
} catch(e) { | ||
return false; | ||
} | ||
parsed.addr_recv = this.getAddr(data.slice(20, 46)); | ||
parsed.addr_from = this.getAddr(data.slice(46, 72)); | ||
parsed.nonce = new Buffer(8); | ||
data.copy(parsed.nonce, 0, 72, 80); | ||
parsed.client = Message.prototype.getVarString(data, 80); | ||
parsed.height = data.readUInt32LE(data.length-4); | ||
console.log('VERSION:', parsed); | ||
if (parsed.nonce.toString('hex') === this.nonce.toString('hex')) { | ||
return parsed; | ||
}; | ||
BTCNetwork.prototype.handleVersionMessage = function handleVersionMessage(data, peer) { | ||
if (data.nonce.toString('hex') === this.nonce.toString('hex')) { | ||
// We connected to ourselves! | ||
@@ -202,4 +375,4 @@ this.manager.delActive(peer, 'connected to self'); | ||
if (this.options.externalIP == false || (this.options.externalIP.toString('hex') != parsed.addr_recv.hostRaw.toString('hex') && parsed.addr_recv.hostRaw.slice(10,16).toHex != '000000000000')) { | ||
this.options.externalIP = parsed.addr_recv.hostRaw; | ||
if (this.options.externalIP == false || (this.options.externalIP.toString('hex') != data.addr_recv.hostRaw.toString('hex') && data.addr_recv.hostRaw.slice(10,16).toHex != '000000000000')) { | ||
this.options.externalIP = data.addr_recv.hostRaw; | ||
this._error('External address discovered to be '+this.options.externalIP.toString('hex'), 'info'); | ||
@@ -211,9 +384,7 @@ } | ||
} | ||
peer.info.version = parsed.version; | ||
peer.info.services = parsed.services; | ||
peer.info.nonce = parsed.nonce; | ||
peer.info.client = parsed.client; | ||
peer.info.knownHeight = parsed.height; | ||
return parsed; | ||
peer.info.version = data.version; | ||
peer.info.services = data.services; | ||
peer.info.nonce = data.nonce; | ||
peer.info.client = data.client; | ||
peer.info.knownHeight = data.height; | ||
}; | ||
@@ -260,3 +431,4 @@ | ||
self.manager.delActive(p, 'Failed to send VERACK after VERSION message'); | ||
}, 10000).unref(); | ||
}, 10000); | ||
watchdog.unref(); | ||
p.once('verackMessage', function() { | ||
@@ -279,2 +451,113 @@ // VERACK received; this peer is active now | ||
// This method call is debounced to better batch the requests. | ||
// It waits until this method has not been called for one second before actually sending the request. | ||
// Send force = true to override this and trigger a request now. | ||
BTCNetwork.prototype.getData = function getData(inventory, peer, callback, force) { | ||
if (!Array.isArray(inventory)) { | ||
inventory = [inventory]; | ||
} | ||
if (typeof peer.info.pendingGetData == 'undefined') { | ||
peer.info.pendingGetData = []; | ||
} | ||
peer.info.pendingGetData = peer.info.pendingGetData.concat(inventory); | ||
// Add callback to the list of functions to notify when this batch of data is ready | ||
if (typeof peer.info.getDataListeners == 'undefined') { | ||
peer.info.getDataListeners = []; | ||
} | ||
if (typeof callback == 'function') { | ||
peer.info.getDataListeners.push(callback); | ||
} | ||
if (force !== true && peer.info.pendingGetData.length < 50) { | ||
clearTimeout(peer.info.getDataTimeout); | ||
var self = this; | ||
peer.info.getDataTimeout = setTimeout(function() { | ||
self.getData([], peer, false, true); // Force send now | ||
}, 1000); | ||
return; | ||
} | ||
// Send the request | ||
var toSend = peer.info.pendingGetData.slice(0); // Make local copy | ||
peer.info.pendingGetData = []; // Clear original | ||
var toCallback = peer.info.getDataListeners.slice(0); // Make local copy | ||
peer.info.getDataListeners = []; // Clear original | ||
if (toSend.length == 0) return; | ||
var waitingFor = toSend.length; | ||
console.log('Sending request for:', toSend); | ||
var msg = new Message(this.options.magic, true); | ||
msg.putVarInt(toSend.length); | ||
for (var i = 0; i < toSend.length; i++) { | ||
var inv = toSend[i]; | ||
var raw = new Buffer(36); | ||
raw.writeUInt32LE(inv.type, 0); | ||
inv.hash.copy(raw, 4); | ||
msg.put(raw); | ||
} | ||
var doCallbacks = function(err, rs) { | ||
console.log('firing callbacks'); | ||
for (var i = 0; i < toCallback.length; i++) { | ||
toCallback[i].call(peer, err, rs); | ||
} | ||
}; | ||
if (this.state == 'shutdown') { | ||
doCallbacks('Shutdown in progress'); // Abort | ||
return; | ||
} | ||
console.log(waitingFor+' items pending'); | ||
var items = []; | ||
var watchdog = setTimeout(function watchdog() { | ||
if (waitingFor > 0) { | ||
doCallbacks('Peer unresponsive'); | ||
} | ||
}, 30000); | ||
watchdog.unref(); | ||
var addResult = function addResult(item) { | ||
console.log('add result fired with '+waitingFor+' to go'); | ||
if (item !== false) { | ||
items.push(item); | ||
} | ||
if (--waitingFor <= 0) { | ||
// We were the last one | ||
peer.removeListener('notfoundMessage', notFoundHandler); | ||
peer.removeListener('txMessage', txHandler); | ||
peer.removeListener('blockMessage', blockHandler); | ||
if (items.length == 0) { | ||
doCallbacks('Not Found', false); | ||
} else { | ||
doCallbacks(false, items); | ||
} | ||
return; | ||
} | ||
console.log('waiting for '+waitingFor+' more objects'); | ||
}; | ||
var self = this; | ||
var notFoundHandler = function notFoundHandler(d) { | ||
var numMissing = d.data.readUInt32LE(0); | ||
console.log(numMissing+' reported missing'); | ||
waitingFor = waitingFor - numMissing + 1; | ||
addResult(false); | ||
}; | ||
var txHandler = function txHandler(d) { | ||
console.log('One transaction came in'); | ||
addResult({'type': 'tx', 'data': self.parseTxMessage(d.data, d.peer)}); | ||
}; | ||
var blockHandler = function blockHandler(d) { | ||
console.log('One block came in'); | ||
// TODO: Parse the result before returning | ||
addResult({'type': 'block', 'data': d.data}); | ||
}; | ||
peer.send('getdata', msg.raw()); | ||
peer.on('notfoundMessage', notFoundHandler); | ||
peer.on('txMessage', txHandler); | ||
peer.on('blockMessage', blockHandler); | ||
}; | ||
BTCNetwork.prototype.addrPoll = function addrPoll() { | ||
@@ -281,0 +564,0 @@ var peers = this.manager.send(5, 'state', 'active', 'getaddr'); |
@@ -96,2 +96,13 @@ var sha256 = require('sha256'); | ||
}; | ||
Message.prototype.varIntSize = function varIntSize(num) { | ||
if (num < 0xfd) { | ||
return 1 | ||
} else if (num <= 0xffff) { | ||
return 3 | ||
} else if (num <= 0xffffffff) { | ||
return 5 | ||
} else { | ||
return 9 | ||
} | ||
}; | ||
Message.prototype.putVarString = function putVarString(str) { | ||
@@ -98,0 +109,0 @@ return this.putVarInt(str.length).putString(str); |
{ | ||
"name": "btc-p2p", | ||
"version": "0.1.1", | ||
"version": "0.2.0", | ||
"description": "Manage a network of Bitcoin peers", | ||
@@ -5,0 +5,0 @@ "keywords": [ |
@@ -12,4 +12,6 @@ # Bitcoin P2P Manager | ||
This is accomplished by several message handlers on the BTCNetwork object, named in the format `parseCommandMessage` (e.g. `parseVersionMessage`, `parseInvMessage`, `parseGetaddrMessage`, etc.). If a handler is not found for a message type, the raw form is bubbled up. To handle additional types of messages, add a handler method named appropriately, and it will be passed `data` and `peer` as arguments. It must return an object; if the result is an array (i.e `inv` messages), return an object with one property set to the array (i.e. `{ items: ARRAY }`). | ||
This is accomplished by several message handlers on the BTCNetwork object, named in the format `parseCommandMessage` (e.g. `parseVersionMessage`, `parseInvMessage`, `parseGetaddrMessage`, etc.). If a handler is not found for a message type, the raw form is bubbled up. To handle additional types of messages, add a parser method named appropriately, and it will be passed `data` as an argument, which is the message payload in Buffer form. It must return an object; if the result is an array (i.e `inv` messages), return an object with one property set to the array (i.e. `{ items: ARRAY }`). | ||
In addition to a parser method, a `handleCommandMessage` method can be defined, which will be passed the parsed result, and `peer` as arguments, where `peer` is the Peer who sent the message. | ||
These parsing methods mean, that if you're subscribed to an event stream that presents raw data (i.e. the `versionMessage` events from an individual Peer), you can parse them like: | ||
@@ -22,4 +24,4 @@ | ||
p.on('versionMessage', function(d) { | ||
var parsed = btc.parseVersionMessage(d.data, d.peer); | ||
var parsed = btc.parseVersionMessage(d.data); | ||
}); | ||
``` |
36591
11
979
25