Skip to content

Get block hashes by timestamp range #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 16, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/services/db.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,14 @@ node.getBlock(blockHash, function(err, block) {
//...
});
```

Get Block Hashes by Timestamp Range

```js
var newest = 1441914000; // Notice time is in seconds not milliseconds
var oldest = 1441911000;

node.getBlockHashesByTimestamp(newest, oldest, function(err, hashes) {
//...
});
```
4 changes: 2 additions & 2 deletions lib/services/address/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ AddressService.dependencies = [
];

AddressService.PREFIXES = {
OUTPUTS: new Buffer('32', 'hex'),
SPENTS: new Buffer('33', 'hex')
OUTPUTS: new Buffer('02', 'hex'),
SPENTS: new Buffer('03', 'hex')
};

AddressService.SPACER_MIN = new Buffer('00', 'hex');
Expand Down
75 changes: 75 additions & 0 deletions lib/services/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ util.inherits(DB, Service);

DB.dependencies = ['bitcoind'];

DB.PREFIXES = {
BLOCKS: new Buffer('01', 'hex')
};

DB.prototype._setDataPath = function() {
$.checkState(this.node.datadir, 'Node is expected to have a "datadir" property');
var regtest = Networks.get('regtest');
Expand Down Expand Up @@ -177,6 +181,7 @@ DB.prototype.transactionHandler = function(txInfo) {
DB.prototype.getAPIMethods = function() {
var methods = [
['getBlock', this, this.getBlock, 1],
['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2],
['getTransaction', this, this.getTransaction, 2],
['getTransactionWithBlockInfo', this, this.getTransactionWithBlockInfo, 2],
['sendTransaction', this, this.sendTransaction, 1],
Expand All @@ -194,6 +199,54 @@ DB.prototype.getBlock = function(hash, callback) {
});
};

/**
* get block hashes between two timestamps
* @param {Number} high - high timestamp, in seconds, inclusive
* @param {Number} low - low timestamp, in seconds, inclusive
* @param {Function} callback
*/
DB.prototype.getBlockHashesByTimestamp = function(high, low, callback) {
var self = this;
var hashes = [];

try {
var lowKey = this._encodeBlockIndexKey(low);
var highKey = this._encodeBlockIndexKey(high);
} catch(e) {
return callback(e);
}

var stream = this.store.createReadStream({
gte: lowKey,
lte: highKey,
reverse: true,
valueEncoding: 'binary',
keyEncoding: 'binary'
});

stream.on('data', function(data) {
hashes.push(self._decodeBlockIndexValue(data.value));
});

var error;

stream.on('error', function(streamError) {
if (streamError) {
error = streamError;
}
});

stream.on('close', function() {
if (error) {
return callback(error);
}

callback(null, hashes);
});

return stream;
};

DB.prototype.getTransaction = function(txid, queryMempool, callback) {
this.node.services.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) {
if (err) {
Expand Down Expand Up @@ -371,6 +424,13 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) {
this.subscriptions.block[i].emit('block', block.hash);
}

// Update block index
operations.push({
type: add ? 'put' : 'del',
key: this._encodeBlockIndexKey(block.header.timestamp),
value: this._encodeBlockIndexValue(block.hash)
});

async.eachSeries(
this.node.services,
function(mod, next) {
Expand Down Expand Up @@ -402,6 +462,21 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) {
);
};

DB.prototype._encodeBlockIndexKey = function(timestamp) {
$.checkArgument(timestamp >= 0 && timestamp <= 4294967295, 'timestamp out of bounds');
var timestampBuffer = new Buffer(4);
timestampBuffer.writeUInt32BE(timestamp);
return Buffer.concat([DB.PREFIXES.BLOCKS, timestampBuffer]);
};

DB.prototype._encodeBlockIndexValue = function(hash) {
return new Buffer(hash, 'hex');
};

DB.prototype._decodeBlockIndexValue = function(value) {
return value.toString('hex');
};

/**
* This function will find the common ancestor between the current chain and a forked block,
* by moving backwards from the forked block until it meets the current chain.
Expand Down
18 changes: 9 additions & 9 deletions test/services/address/index.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,13 @@ describe('Address Service', function() {
should.not.exist(err);
operations.length.should.equal(81);
operations[0].type.should.equal('put');
operations[0].key.toString('hex').should.equal('3202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac');
operations[3].type.should.equal('put');
operations[3].key.toString('hex').should.equal('33fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000');
operations[64].type.should.equal('put');
operations[64].key.toString('hex').should.equal('329780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
operations[64].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
operations[64].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac');
done();
});
Expand All @@ -149,13 +149,13 @@ describe('Address Service', function() {
should.not.exist(err);
operations.length.should.equal(81);
operations[0].type.should.equal('del');
operations[0].key.toString('hex').should.equal('3202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac');
operations[3].type.should.equal('del');
operations[3].key.toString('hex').should.equal('33fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000');
operations[64].type.should.equal('del');
operations[64].key.toString('hex').should.equal('329780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
operations[64].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
operations[64].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac');
done();
});
Expand Down Expand Up @@ -563,7 +563,7 @@ describe('Address Service', function() {
});
createReadStreamCallCount.should.equal(1);
var data = {
key: new Buffer('32038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex')
};
testStream.emit('data', data);
Expand Down Expand Up @@ -611,12 +611,12 @@ describe('Address Service', function() {
});

var data1 = {
key: new Buffer('32038a213afdfc551fc658e9a2a58a86e98d69b68700000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68700000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex')
};

var data2 = {
key: new Buffer('32038a213afdfc551fc658e9a2a58a86e98d69b68700000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'),
key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68700000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'),
value: new Buffer('40c388000000000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex')
};

Expand Down
94 changes: 88 additions & 6 deletions test/services/db.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,76 @@ describe('DB Service', function() {
});
});

describe('#getBlockHashesByTimestamp', function() {
it('should get the correct block hashes', function(done) {
var db = new DB(baseConfig);
var readStream = new EventEmitter();
db.store = {
createReadStream: sinon.stub().returns(readStream)
};

var block1 = {
hash: '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b',
timestamp: 1441911909
};

var block2 = {
hash: '000000000383752a55a0b2891ce018fd0fdc0b6352502772b034ec282b4a1bf6',
timestamp: 1441913112
};

db.getBlockHashesByTimestamp(1441914000, 1441911000, function(err, hashes) {
should.not.exist(err);
hashes.should.deep.equal([block2.hash, block1.hash]);
done();
});

readStream.emit('data', {
key: db._encodeBlockIndexKey(block2.timestamp),
value: db._encodeBlockIndexValue(block2.hash)
});

readStream.emit('data', {
key: db._encodeBlockIndexKey(block1.timestamp),
value: db._encodeBlockIndexValue(block1.hash)
});

readStream.emit('close');
});

it('should give an error if the stream has an error', function(done) {
var db = new DB(baseConfig);
var readStream = new EventEmitter();
db.store = {
createReadStream: sinon.stub().returns(readStream)
};

db.getBlockHashesByTimestamp(1441911000, 1441914000, function(err, hashes) {
should.exist(err);
err.message.should.equal('error');
done();
});

readStream.emit('error', new Error('error'));

readStream.emit('close');
});

it('should give an error if the timestamp is out of range', function(done) {
var db = new DB(baseConfig);
var readStream = new EventEmitter();
db.store = {
createReadStream: sinon.stub().returns(readStream)
};

db.getBlockHashesByTimestamp(-1, -5, function(err, hashes) {
should.exist(err);
err.message.should.equal('Invalid Argument: timestamp out of bounds');
done();
});
});
});

describe('#getPrevHash', function() {
it('should return prevHash from bitcoind', function(done) {
var db = new DB(baseConfig);
Expand Down Expand Up @@ -650,10 +720,22 @@ describe('DB Service', function() {
batch: sinon.stub().callsArg(1)
};

var block = {
hash: '00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863',
header: {
timestamp: 1441906365
}
};

it('should call blockHandler in all services and perform operations', function(done) {
db.runAllBlockHandlers('block', true, function(err) {
db.runAllBlockHandlers(block, true, function(err) {
should.not.exist(err);
db.store.batch.args[0][0].should.deep.equal(['op1', 'op2', 'op3', 'op4', 'op5']);
var blockOp = {
type: 'put',
key: db._encodeBlockIndexKey(1441906365),
value: db._encodeBlockIndexValue('00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863')
};
db.store.batch.args[0][0].should.deep.equal([blockOp, 'op1', 'op2', 'op3', 'op4', 'op5']);
done();
});
});
Expand All @@ -663,7 +745,7 @@ describe('DB Service', function() {
Service3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error'));
db.node.services.service3 = new Service3();

db.runAllBlockHandlers('block', true, function(err) {
db.runAllBlockHandlers(block, true, function(err) {
should.exist(err);
done();
});
Expand All @@ -675,7 +757,7 @@ describe('DB Service', function() {
service3: new Service3()
};

db.runAllBlockHandlers('block', true, function(err) {
db.runAllBlockHandlers(block, true, function(err) {
should.not.exist(err);
done();
});
Expand All @@ -688,7 +770,7 @@ describe('DB Service', function() {
};

(function() {
db.runAllBlockHandlers('block', true, function(err) {
db.runAllBlockHandlers(block, true, function(err) {
should.not.exist(err);
});
}).should.throw('bitcore.ErrorInvalidArgument');
Expand All @@ -701,7 +783,7 @@ describe('DB Service', function() {
db.node = {};
db.node.services = {};
var methods = db.getAPIMethods();
methods.length.should.equal(5);
methods.length.should.equal(6);
});
});

Expand Down