This commit is contained in:
Fedor Indutny 2014-04-30 12:44:59 +04:00
parent 106d243873
commit a20cb1688f
12 changed files with 826 additions and 21 deletions

View File

@ -3,5 +3,9 @@ var elliptic = require('elliptic');
bcoin.ecdsa = elliptic.ecdsa(elliptic.nist.secp256k1);
bcoin.utils = require('./bcoin/utils');
bcoin.bloom = require('./bcoin/bloom');
bcoin.protocol = require('./bcoin/protocol');
bcoin.tx = require('./bcoin/tx');
bcoin.block = require('./bcoin/block');
bcoin.wallet = require('./bcoin/wallet');
bcoin.peer = require('./bcoin/peer');

42
lib/bcoin/block.js Normal file
View File

@ -0,0 +1,42 @@
var bcoin = require('../bcoin');
var utils = bcoin.utils;
function Block(data) {
if (!(this instanceof Block))
return new Block(data);
this.type = 'block';
this.version = data.version;
this.prevBlock = data.prevBlock;
this.merkleRoot = data.merkleRoot;
this.ts = data.ts;
this.bits = data.bits;
this.nonce = data.nonce;
this._hash = null;
}
module.exports = Block;
Block.prototype.hash = function hash(enc) {
// Hash it
if (!this._hash)
this._hash = utils.dsha256(this.abbr());
return enc === 'hex' ? utils.toHex(this._hash) : this._hash;
};
Block.prototype.abbr = function abbr() {
var res = new Array(80);
utils.writeU32(res, this.version, 0);
utils.copy(this.prevBlock, res, 4);
utils.copy(this.merkleRoot, res, 36);
utils.writeU32(res, this.ts, 68);
utils.writeU32(res, this.bits, 72);
utils.writeU32(res, this.nonce, 76);
return res;
};
Block.prototype.render = function render(framer) {
console.log('here');
return [];
};

139
lib/bcoin/bloom.js Normal file
View File

@ -0,0 +1,139 @@
var bcoin = require('../bcoin');
var utils = bcoin.utils;
function Bloom(size, n, tweak) {
if (!(this instanceof Bloom))
return new Bloom(size, n, tweak);
this.filter = new Array(Math.ceil(size / 32));
for (var i = 0; i < this.filter.length; i++)
this.filter[i] = 0;
this.size = size;
this.n = n;
this.tweak = tweak;
}
module.exports = Bloom;
Bloom.prototype.hash = function hash(val, n) {
return Bloom.hash(val, sum32(mul32(n, 0xfba4c795), this.tweak)) % this.size;
};
Bloom.prototype.add = function add(val) {
for (var i = 0; i < this.n; i++) {
var bit = this.hash(val, i);
var pos = 1 << (bit & 0x1f);
var shift = bit >> 5;
this.filter[shift] |= pos;
}
};
Bloom.prototype.test = function test(val) {
for (var i = 0; i < this.n; i++) {
var bit = this.hash(val, i);
var pos = 1 << (bit & 0x1f);
var shift = bit >> 5;
if ((this.filter[shift] & pos) === 0)
return false;
}
return true;
};
Bloom.prototype.toArray = function toArray() {
var bytes = Math.ceil(this.size / 8);
var res = new Array(this.filter.length * 4);
for (var i = 0; i < this.filter.length; i++) {
var w = this.filter[i];
res[i * 4] = w & 0xff;
res[i * 4 + 1] = (w >> 8) & 0xff;
res[i * 4 + 2] = (w >> 16) & 0xff;
res[i * 4 + 3] = (w >> 24) & 0xff;
}
return res.slice(0, bytes);
};
function mul32(a, b) {
var alo = a & 0xffff;
var blo = b & 0xffff;
var ahi = a >>> 16;
var bhi = b >>> 16;
var lo = alo * blo;
var hi = (ahi * blo + bhi * alo) & 0xffff;
hi += lo >>> 16;
lo &= 0xffff;
var r = (hi << 16) | lo;
if (r < 0)
r += 0x100000000;
return r;
}
function sum32(a, b) {
var r = (a + b) & 0xffffffff;
if (r < 0)
r += 0x100000000;
return r;
}
function rotl32(w, b) {
return (w << b) | (w >>> (32 - b));
}
function hash(data, seed) {
data = utils.toArray(data);
var c1 = 0xcc9e2d51;
var c2 = 0x1b873593;
var r1 = 15;
var r2 = 13;
var m = 5;
var n = 0xe6546b64;
var hash = seed;
for (var i = 0; i + 4 <= data.length; i += 4) {
var w = data[i] |
(data[i + 1] << 8) |
(data[i + 2] << 16) |
(data[i + 3] << 24);
w = mul32(w, c1);
w = rotl32(w, r1);
w = mul32(w, c2);
hash ^= w;
hash = rotl32(hash, r2);
hash = mul32(hash, m);
hash = sum32(hash, n);
}
if (i !== data.length) {
var r = 0;
for (var j = data.length - 1; j >= i; j--)
r = (r << 8) | data[j];
r = mul32(r, c1);
r = rotl32(r, r1);
if (r < 0)
r += 0x100000000;
r = mul32(r, c2);
hash ^= r;
}
hash ^= data.length;
hash ^= hash >>> 16;
hash = mul32(hash, 0x85ebca6b);
hash ^= hash >>> 13;
hash = mul32(hash, 0xc2b2ae35);
hash ^= hash >>> 16;
if (hash < 0)
hash += 0x100000000;
return hash;
}
Bloom.hash = hash;

312
lib/bcoin/peer.js Normal file
View File

@ -0,0 +1,312 @@
var assert = require('assert');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var bcoin = require('../bcoin');
var utils = bcoin.utils;
var constants = bcoin.protocol.constants;
// Browserify, I'm looking at you
try {
var NodeBuffer = require('buf' + 'fer').Buffer;
} catch (e) {
}
function Peer(socket, options) {
if (!(this instanceof Peer))
return new Peer(socket, options);
EventEmitter.call(this);
this.socket = socket;
this.parser = new bcoin.protocol.parser();
this.framer = new bcoin.protocol.framer();
this.bloom = new bcoin.bloom(8 * 10 * 1024,
10,
(Math.random() * 0xffffffff) | 0),
this.version = null;
this.destroyed = false;
this.options = options || {};
this._broadcast = {
timout: this.options.broadcastTimeout || 30000,
map: {}
};
this._request = {
timeout: this.options.requestTimeout || 30000,
queue: []
};
this._ping = {
timer: null,
interval: this.options.pingInterval || 5000
};
this._init();
}
util.inherits(Peer, EventEmitter);
module.exports = Peer;
Peer.prototype._init = function init() {
var self = this;
this.socket.once('error', function(err) {
self._error(err);
});
this.socket.once('close', function() {
self._error('socket hangup');
});
this.socket.on('data', function(chunk) {
self.parser.feed(chunk);
});
this.parser.on('packet', function(packet) {
self._onPacket(packet);
});
this.parser.on('error', function(err) {
self._error(err);
});
this._ping.timer = setInterval(function() {
self._write(self.framer.ping([
0xde, 0xad, 0xbe, 0xef,
0xde, 0xad, 0xbe, 0xef
]));
}, this._ping.interval);
// Send hello
this._write(this.framer.version());
this._req('verack', function(err, payload) {
if (err)
return self._error(err);
self.emit('ack');
});
};
Peer.prototype.broadcast = function broadcast(items) {
if (!Array.isArray(items))
items = [ items ];
var self = this;
items.forEach(function(item) {
var key = item.hash('hex');
var old = this._broadcast.map[key];
if (old)
clearTimeout(old.timer);
// Auto-cleanup broadcast map after timeout
var entry = {
timeout: setTimeout(function() {
delete self._broadcast.map[key];
}, this._broadcast.timout),
value: item
};
this._broadcast.map[key] = entry;
}, this);
this._write(this.framer.inv(items));
};
Peer.prototype.watch = function watch(id) {
this.bloom.add(id);
this._write(this.framer.filterLoad(this.bloom, 'pubkeyOnly'));
};
Peer.prototype.loadBlocks = function loadBlocks() {
if (this.loadingBlocks)
return;
this.loadingBlocks = true;
this._write(this.framer.getBlocks([ constants.genesis ]));
};
Peer.prototype.destroy = function destroy() {
if (this.destroyed)
return;
this.destroyed = true;
this.socket.destroy();
this.socket = null;
// Clean-up timeouts
Object.keys(this._broadcast.map).forEach(function(key) {
clearTimeout(this._broadcast.map[key].timer);
}, this);
clearInterval(this._ping.timer);
this._ping.timer = null;
};
// Private APIs
Peer.prototype._write = function write(chunk) {
if (NodeBuffer)
this.socket.write(new NodeBuffer(chunk));
else
this.socket.write(chunk);
};
Peer.prototype._error = function error(err) {
if (this.destroyed)
return;
this.destroy();
this.emit('error', typeof err === 'string' ? new Error(err) : err);
};
Peer.prototype._req = function _req(cmd, cb) {
var self = this;
var entry = {
cmd: cmd,
cb: cb,
ontimeout: function() {
var i = self._request.queue.indexOf(entry);
if (i !== -1)
self.request.queue.splice(i, 1);
cb(new Error('Timed out'), null);
},
timer: null
};
entry.timer = setTimeout(entry.ontimeout, this._request.timeout)
this._request.queue.push(entry);
};
Peer.prototype._res = function _res(cmd, payload) {
var entry = this._request.queue[0];
if (!entry || entry.cmd && entry.cmd !== cmd)
return;
var res = entry.cb(null, payload, cmd);
// If callback returns false - it hasn't finished processing responses
if (res === false) {
assert(!entry.cmd);
// Restart timer
entry.timer = setTimeout(entry.ontimeout, this._request.timeout)
} else {
this._request.queue.shift();
clearTimeout(entry.timer);
entry.timer = null;
}
};
Peer.prototype._getData = function _getData(items, cb) {
var map = {};
var waiting = items.length;
items.forEach(function(item) {
map[utils.toHex(item.hash)] = {
item: item,
once: false,
result: null
};
});
var self = this;
function markEntry(hash, result) {
var entry = map[utils.toHex(hash)];
if (!entry || entry.once) {
done(new Error('Invalid notfound entry hash'));
return false;
}
entry.once = true;
entry.result = result;
waiting--;
return true;
}
this._write(this.framer.getData(items));
// Process all incoming data, until all data is returned
this._req(null, function(err, payload, cmd) {
var ok = true;
if (cmd === 'notfound') {
ok = payload.every(function(item) {
return markEntry(item.hash, null);
});
} else if (cmd === 'tx') {
var tx = bcoin.tx(payload);
ok = markEntry(tx.hash(), b);
} else if (cmd === 'merkleblock') {
var b = bcoin.block(payload);
ok = markEntry(b.hash(), b);
} else if (cmd === 'block') {
var b = bcoin.block(payload);
ok = markEntry(b.hash(), b);
} else {
done(new Error('Unknown packet in reply to getdata: ' + cmd));
return;
}
if (!ok)
return;
if (waiting === 0)
done();
else
return false;
});
function done(err) {
if (err)
return cb(err);
cb(null, items.map(function(item) {
return map[utils.toHex(item.hash)].result;
}));
}
};
Peer.prototype._onPacket = function onPacket(packet) {
var cmd = packet.cmd;
var payload = packet.payload;
if (cmd === 'version')
return this._handleVersion(payload);
else if (cmd === 'inv')
return this._handleInv(payload);
else if (cmd === 'getdata')
return this._handleGetData(payload);
else
return this._res(cmd, payload);
};
Peer.prototype._handleVersion = function handleVersion(payload) {
if (payload.v < constants.minVersion)
return this._error('peer doesn\'t support required protocol version');
// ACK
this._write(this.framer.verack());
this.version = payload;
};
Peer.prototype._handleGetData = function handleGetData(items) {
items.forEach(function(item) {
// Filter out not broadcasted things
var hash = utils.toHex(item.hash);
if (!this._broadcast.map[hash])
return;
var entry = this._broadcast.map[hash].value;
this._write(entry.render(this.framer));
}, this);
};
Peer.prototype._handleInv = function handleInv(items) {
// Always request what advertised
var req = items.filter(function(item) {
return item.type === 'tx' || item.type === 'block';
}).map(function(item) {
if (item.type === 'tx')
return item;
if (item.type === 'block')
return { type: 'filtered', hash: item.hash };
});
var self = this;
this._getData(req, function(err, data) {
if (err)
return self._error(err);
console.log(data.join(', '));
});
};
Peer.prototype._handleMerkleBlock = function handleMerkleBlock(block) {
console.log(utils.toHex(block.prevBlock));
};

View File

@ -1,8 +1,33 @@
var bcoin = require('../../bcoin');
var utils = bcoin.utils;
exports.minVersion = 70001;
exports.version = 70002;
exports.magic = 0xd9b4bef9;
exports.genesis = utils.toArray(
'000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f', 'hex');
// version - services field
exports.services = {
network: 1
};
exports.inv = {
error: 0,
tx: 1,
block: 2,
filtered: 3
};
exports.invByVal = {
0: 'error',
1: 'tx',
2: 'block',
3: 'filtered'
};
exports.filterFlags = {
none: 0,
all: 1,
pubkeyOnly: 2
};

View File

@ -37,7 +37,7 @@ Framer.prototype.header = function header(cmd, payload) {
};
Framer.prototype.packet = function packet(cmd, payload) {
var h = this.header('version', payload);
var h = this.header(cmd, payload);
return h.concat(payload);
};
@ -89,3 +89,114 @@ Framer.prototype.version = function version(packet) {
return this.packet('version', p);
};
Framer.prototype.verack = function verack() {
return this.packet('verack', []);
};
function varint(arr, value, off) {
if (!off)
off = 0;
if (value < 0xfd) {
arr[off] = value;
return 1;
} else if (value <= 0xffff) {
arr[off] = 0xfd;
arr[off + 1] = value & 0xff;
arr[off + 2] = value >>> 8;
return 3;
} else if (value <= 0xffffffff) {
arr[off] = 0xfd;
arr[off + 3] = value & 0xff;
arr[off + 4] = (value >>> 8) & 0xff;
arr[off + 5] = (value >>> 16) & 0xff;
arr[off + 6] = value >>> 24;
return 5;
} else {
throw new Error('64bit varint not supported yet');
}
}
Framer.prototype._inv = function _inv(command, items) {
var res = [];
var off = varint(res, items.length, 0);
for (var i = 0; i < items.length; i++) {
// Type
off += writeU32(res, constants.inv[items[i].type], off);
// Hash
var hash = items[i].hash;
assert.equal(hash.length, 32);
res = res.concat(hash);
off += hash.length;
}
return this.packet(command, res);
};
Framer.prototype.inv = function inv(items) {
return this._inv('inv', items);
};
Framer.prototype.getData = function getData(items) {
return this._inv('getdata', items);
};
Framer.prototype.notFound = function notFound(items) {
return this._inv('notfound', items);
};
Framer.prototype.ping = function ping(nonce) {
return this.packet('ping', nonce);
};
Framer.prototype.pong = function pong(nonce) {
return this.packet('pong', nonce.slice(0, 8));
};
Framer.prototype.filterLoad = function filterLoad(bloom, update) {
var filter = bloom.toArray();
var before = [];
varint(before, filter.length, 0)
var after = new Array(9);
// Number of hash functions
writeU32(after, bloom.n, 0);
// nTweak
writeU32(after, bloom.tweak, 4);
// nFlags
after[8] = constants.filterFlags[update];
var r = this.packet('filterload', before.concat(filter, after));
return r;
};
Framer.prototype.filterClear = function filterClear() {
return this.packet('filterclear', []);
};
Framer.prototype.getBlocks = function getBlocks(hashes, stop) {
var p = [];
writeU32(p, constants.version, 0);
var off = 4 + varint(p, hashes.length, 4);
p.length = off + 32 * (hashes.length + 1);
for (var i = 0; i < hashes.length; i++) {
var len = utils.copy(hashes[i], p, off);
for (; len < 32; len++)
p[off + len] = 0;
off += len;
}
var len = stop ? utils.copy(stop, p, off) : 0;
for (; len < 32; len++)
p[off + len] = 0;
assert.equal(off + len, p.length);
return this.packet('getblocks', p);
};

View File

@ -23,11 +23,9 @@ function Parser() {
util.inherits(Parser, EventEmitter);
module.exports = Parser;
Parser.prototype.execute = function(data) {
if (data) {
this.pendingTotal += data.length;
this.pending.push(data);
}
Parser.prototype.feed = function feed(data) {
this.pendingTotal += data.length;
this.pending.push(data);
while (this.pendingTotal >= this.waiting) {
// Concat chunks
var chunk = new Array(this.waiting);
@ -88,6 +86,10 @@ Parser.prototype.parseHeader = function parseHeader(h) {
Parser.prototype.parsePayload = function parsePayload(cmd, p) {
if (cmd === 'version')
return this.parseVersion(p);
else if (cmd === 'getdata' || cmd === 'inv' || cmd === 'notfound')
return this.parseInvList(p);
else if (cmd === 'merkleblock')
return this.parseMerkleBlock(p);
else
return p;
};
@ -97,9 +99,6 @@ Parser.prototype.parseVersion = function parseVersion(p) {
return this.emit('error', new Error('version packet is too small'));
var v = readU32(p, 0);
if (v < constants.minVersion)
return this.emit('error', new Error('version number is too small'));
var services = readU64(p, 4);
// Timestamp
@ -123,3 +122,60 @@ Parser.prototype.parseVersion = function parseVersion(p) {
relay: relay
};
};
function readIntv(p, off) {
if (!off)
off = 0;
var r, bytes;
if (p[off] < 0xfd) {
r = p[off];
bytes = 1;
} else if (p[off] === 0xfd) {
r = p[off + 1] | (p[off + 2] << 8);
bytes = 3;
} else if (p[off] === 0xfe) {
r = readU32(p, off + 1);
bytes = 5;
} else {
r = 0;
bytes = 9;
}
return { off: bytes, r: r };
}
Parser.prototype.parseInvList = function parseInvList(p) {
var count = readIntv(p, 0);
p = p.slice(count.off);
count = count.r;
if (p.length < count * 36)
return this.emit('error', new Error('Invalid getdata size'));
var items = [];
for (var i = 0, off = 0; i < count; i++, off += 36) {
items.push({
type: constants.invByVal[readU32(p, off)],
hash: p.slice(off + 4, off + 36)
});
}
return items;
};
Parser.prototype.parseMerkleBlock = function parsMerkleBlock(p) {
if (p.length < 84)
return this.emit('error', new Error('Invalid merkleblock size'));
return {
version: readU32(p, 0),
prevBlock: p.slice(4, 36),
merkleRoot: p.slice(36, 68),
ts: readU32(p, 68),
bits: readU32(p, 72),
nonce: readU32(p, 76),
totalTx: readU32(p, 80)
// hashes:
// flags:
};
};

28
lib/bcoin/tx.js Normal file
View File

@ -0,0 +1,28 @@
var bcoin = require('../bcoin');
var utils = bcoin.utils;
function TX(data) {
if (!(this instanceof TX))
return new TX(data);
this.type = 'tx';
this._hash = null;
this._raw = data || null;
}
module.exports = TX;
TX.prototype.hash = function hash(enc) {
if (!this._hash) {
// First, obtain the raw TX data
this.render();
// Hash it
this._hash = utils.dsha256(this._raw);
}
return enc === 'hex' ? utils.toHex(this._hash) : this._hash;
};
TX.prototype.render = function render(framer) {
console.log('here');
return [];
};

View File

@ -4,6 +4,37 @@ var assert = require('assert');
var bn = require('bn.js');
var hash = require('hash.js');
function toArray(msg, enc) {
if (Array.isArray(msg))
return msg.slice();
if (!msg)
return [];
var res = [];
if (typeof msg === 'string') {
if (!enc) {
for (var i = 0; i < msg.length; i++) {
var c = msg.charCodeAt(i);
var hi = c >> 8;
var lo = c & 0xff;
if (hi)
res.push(hi, lo);
else
res.push(lo);
}
} else if (enc === 'hex') {
msg = msg.replace(/[^a-z0-9]+/ig, '');
if (msg.length % 2 != 0)
msg = '0' + msg;
for (var i = 0; i < msg.length; i += 2)
res.push(parseInt(msg[i] + msg[i + 1], 16));
}
} else {
for (var i = 0; i < msg.length; i++)
res[i] = msg[i] | 0;
}
return res;
}
utils.toArray = toArray;
var base58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZ' +
'abcdefghijkmnopqrstuvwxyz';
@ -82,13 +113,17 @@ utils.ripesha = function ripesha(data, enc) {
};
utils.checksum = function checksum(data, enc) {
return utils.sha256(utils.sha256(data, enc)).slice(0, 4);
return utils.dsha256(data, enc).slice(0, 4);
};
utils.sha256 = function sha256(data, enc) {
return hash.sha256().update(data, enc).digest();
};
utils.dsha256 = function dsha256(data, enc) {
return utils.sha256(utils.sha256(data, enc));
};
utils.readU32 = function readU32(arr, off) {
if (!off)
off = 0;
@ -114,6 +149,7 @@ utils.writeU32 = function writeU32(dst, num, off) {
dst[off + 1] = (num >>> 8) & 0xff;
dst[off + 2] = (num >>> 16) & 0xff;
dst[off + 3] = (num >>> 24) & 0xff;
return 4;
};
utils.writeAscii = function writeAscii(dst, str, off) {
@ -137,3 +173,18 @@ utils.stringify = function stringify(arr) {
res += String.fromCharCode(arr[i]);
return res;
};
function zero2(word) {
if (word.length === 1)
return '0' + word;
else
return word;
}
function toHex(msg) {
var res = '';
for (var i = 0; i < msg.length; i++)
res += zero2(msg[i].toString(16));
return res;
}
utils.toHex = toHex;

View File

@ -21,8 +21,8 @@
},
"homepage": "https://github.com/indutny/bcoin",
"dependencies": {
"bn.js": "^0.1.7",
"elliptic": "^0.6.0",
"bn.js": "^0.2.0",
"elliptic": "^0.7.0",
"hash.js": "^0.2.0"
},
"devDependencies": {

29
test/bloom-test.js Normal file
View File

@ -0,0 +1,29 @@
var assert = require('assert');
var bcoin = require('../');
describe('Bloom', function() {
it('should do proper murmur3', function() {
var h = bcoin.bloom.hash;
assert.equal(h('', 0), 0);
assert.equal(h('', 0xfba4c795), 0x6a396f08);
assert.equal(h('00', 0xfba4c795), 0x2a101837);
assert.equal(h('hello world', 0), 0x5e928f0f);
});
it('should test and add stuff', function() {
var b = bcoin.bloom(512, 10, 156);
b.add('hello');
assert(b.test('hello'));
assert(!b.test('hello!'));
assert(!b.test('ping'));
b.add('hello!');
assert(b.test('hello!'));
assert(!b.test('ping'));
b.add('ping');
assert(b.test('ping'));
});
});

View File

@ -9,15 +9,23 @@ describe('Protocol', function() {
framer = bcoin.protocol.framer();
});
it('should encode/decode version packet', function(cb) {
var ver = framer.version();
parser.once('packet', function(packet) {
assert.equal(packet.cmd, 'version');
assert.equal(packet.payload.v, 70002);
assert.equal(packet.payload.relay, false);
cb();
function packetTest(command, payload, test) {
it('should encode/decode ' + command, function(cb) {
var ver = framer[command]();
parser.once('packet', function(packet) {
assert.equal(packet.cmd, command);
test(packet.payload);
cb();
});
parser.feed(ver);
});
parser.execute(ver);
}
packetTest('version', {}, function(payload) {
assert.equal(payload.v, 70002);
assert.equal(payload.relay, false);
});
packetTest('verack', {}, function(payload) {
});
});