Skip to content

Commit 4343b90

Browse files
author
Braydon Fuller
committed
Merge pull request #219 from pnagurny/feature/timestamp-blocks
Get block hashes by timestamp range
2 parents aa9504a + a0be38f commit 4343b90

File tree

5 files changed

+185
-17
lines changed

5 files changed

+185
-17
lines changed

docs/services/db.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,14 @@ node.getBlock(blockHash, function(err, block) {
3232
//...
3333
});
3434
```
35+
36+
Get Block Hashes by Timestamp Range
37+
38+
```js
39+
var newest = 1441914000; // Notice time is in seconds not milliseconds
40+
var oldest = 1441911000;
41+
42+
node.getBlockHashesByTimestamp(newest, oldest, function(err, hashes) {
43+
//...
44+
});
45+
```

lib/services/address/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ AddressService.dependencies = [
3535
];
3636

3737
AddressService.PREFIXES = {
38-
OUTPUTS: new Buffer('32', 'hex'),
39-
SPENTS: new Buffer('33', 'hex')
38+
OUTPUTS: new Buffer('02', 'hex'),
39+
SPENTS: new Buffer('03', 'hex')
4040
};
4141

4242
AddressService.SPACER_MIN = new Buffer('00', 'hex');

lib/services/db.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ util.inherits(DB, Service);
6060

6161
DB.dependencies = ['bitcoind'];
6262

63+
DB.PREFIXES = {
64+
BLOCKS: new Buffer('01', 'hex')
65+
};
66+
6367
DB.prototype._setDataPath = function() {
6468
$.checkState(this.node.datadir, 'Node is expected to have a "datadir" property');
6569
var regtest = Networks.get('regtest');
@@ -177,6 +181,7 @@ DB.prototype.transactionHandler = function(txInfo) {
177181
DB.prototype.getAPIMethods = function() {
178182
var methods = [
179183
['getBlock', this, this.getBlock, 1],
184+
['getBlockHashesByTimestamp', this, this.getBlockHashesByTimestamp, 2],
180185
['getTransaction', this, this.getTransaction, 2],
181186
['getTransactionWithBlockInfo', this, this.getTransactionWithBlockInfo, 2],
182187
['sendTransaction', this, this.sendTransaction, 1],
@@ -194,6 +199,54 @@ DB.prototype.getBlock = function(hash, callback) {
194199
});
195200
};
196201

202+
/**
203+
* get block hashes between two timestamps
204+
* @param {Number} high - high timestamp, in seconds, inclusive
205+
* @param {Number} low - low timestamp, in seconds, inclusive
206+
* @param {Function} callback
207+
*/
208+
DB.prototype.getBlockHashesByTimestamp = function(high, low, callback) {
209+
var self = this;
210+
var hashes = [];
211+
212+
try {
213+
var lowKey = this._encodeBlockIndexKey(low);
214+
var highKey = this._encodeBlockIndexKey(high);
215+
} catch(e) {
216+
return callback(e);
217+
}
218+
219+
var stream = this.store.createReadStream({
220+
gte: lowKey,
221+
lte: highKey,
222+
reverse: true,
223+
valueEncoding: 'binary',
224+
keyEncoding: 'binary'
225+
});
226+
227+
stream.on('data', function(data) {
228+
hashes.push(self._decodeBlockIndexValue(data.value));
229+
});
230+
231+
var error;
232+
233+
stream.on('error', function(streamError) {
234+
if (streamError) {
235+
error = streamError;
236+
}
237+
});
238+
239+
stream.on('close', function() {
240+
if (error) {
241+
return callback(error);
242+
}
243+
244+
callback(null, hashes);
245+
});
246+
247+
return stream;
248+
};
249+
197250
DB.prototype.getTransaction = function(txid, queryMempool, callback) {
198251
this.node.services.bitcoind.getTransaction(txid, queryMempool, function(err, txBuffer) {
199252
if (err) {
@@ -371,6 +424,13 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) {
371424
this.subscriptions.block[i].emit('block', block.hash);
372425
}
373426

427+
// Update block index
428+
operations.push({
429+
type: add ? 'put' : 'del',
430+
key: this._encodeBlockIndexKey(block.header.timestamp),
431+
value: this._encodeBlockIndexValue(block.hash)
432+
});
433+
374434
async.eachSeries(
375435
this.node.services,
376436
function(mod, next) {
@@ -402,6 +462,21 @@ DB.prototype.runAllBlockHandlers = function(block, add, callback) {
402462
);
403463
};
404464

465+
DB.prototype._encodeBlockIndexKey = function(timestamp) {
466+
$.checkArgument(timestamp >= 0 && timestamp <= 4294967295, 'timestamp out of bounds');
467+
var timestampBuffer = new Buffer(4);
468+
timestampBuffer.writeUInt32BE(timestamp);
469+
return Buffer.concat([DB.PREFIXES.BLOCKS, timestampBuffer]);
470+
};
471+
472+
DB.prototype._encodeBlockIndexValue = function(hash) {
473+
return new Buffer(hash, 'hex');
474+
};
475+
476+
DB.prototype._decodeBlockIndexValue = function(value) {
477+
return value.toString('hex');
478+
};
479+
405480
/**
406481
* This function will find the common ancestor between the current chain and a forked block,
407482
* by moving backwards from the forked block until it meets the current chain.

test/services/address/index.unit.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,13 @@ describe('Address Service', function() {
126126
should.not.exist(err);
127127
operations.length.should.equal(81);
128128
operations[0].type.should.equal('put');
129-
operations[0].key.toString('hex').should.equal('3202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
129+
operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
130130
operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac');
131131
operations[3].type.should.equal('put');
132-
operations[3].key.toString('hex').should.equal('33fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
132+
operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
133133
operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000');
134134
operations[64].type.should.equal('put');
135-
operations[64].key.toString('hex').should.equal('329780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
135+
operations[64].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
136136
operations[64].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac');
137137
done();
138138
});
@@ -149,13 +149,13 @@ describe('Address Service', function() {
149149
should.not.exist(err);
150150
operations.length.should.equal(81);
151151
operations[0].type.should.equal('del');
152-
operations[0].key.toString('hex').should.equal('3202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
152+
operations[0].key.toString('hex').should.equal('0202a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b00000543abfdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e1692300000000');
153153
operations[0].value.toString('hex').should.equal('41e2a49ec1c0000076a91402a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b88ac');
154154
operations[3].type.should.equal('del');
155-
operations[3].key.toString('hex').should.equal('33fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
155+
operations[3].key.toString('hex').should.equal('03fdbd324b28ea69e49c998816407dc055fb81d06e00000543ab3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a900000020');
156156
operations[3].value.toString('hex').should.equal('5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca00000000');
157157
operations[64].type.should.equal('del');
158-
operations[64].key.toString('hex').should.equal('329780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
158+
operations[64].key.toString('hex').should.equal('029780ccd5356e2acc0ee439ee04e0fe69426c752800000543abe66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d00000001');
159159
operations[64].value.toString('hex').should.equal('4147a6b00000000076a9149780ccd5356e2acc0ee439ee04e0fe69426c752888ac');
160160
done();
161161
});
@@ -563,7 +563,7 @@ describe('Address Service', function() {
563563
});
564564
createReadStreamCallCount.should.equal(1);
565565
var data = {
566-
key: new Buffer('32038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
566+
key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b687000000000f125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
567567
value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex')
568568
};
569569
testStream.emit('data', data);
@@ -611,12 +611,12 @@ describe('Address Service', function() {
611611
});
612612

613613
var data1 = {
614-
key: new Buffer('32038a213afdfc551fc658e9a2a58a86e98d69b68700000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
614+
key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68700000543a8125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf8700000001', 'hex'),
615615
value: new Buffer('41f0de058a80000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex')
616616
};
617617

618618
var data2 = {
619-
key: new Buffer('32038a213afdfc551fc658e9a2a58a86e98d69b68700000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'),
619+
key: new Buffer('02038a213afdfc551fc658e9a2a58a86e98d69b68700000543ac3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae700000002', 'hex'),
620620
value: new Buffer('40c388000000000076a914038a213afdfc551fc658e9a2a58a86e98d69b68788ac', 'hex')
621621
};
622622

test/services/db.unit.js

Lines changed: 88 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,76 @@ describe('DB Service', function() {
369369
});
370370
});
371371

372+
describe('#getBlockHashesByTimestamp', function() {
373+
it('should get the correct block hashes', function(done) {
374+
var db = new DB(baseConfig);
375+
var readStream = new EventEmitter();
376+
db.store = {
377+
createReadStream: sinon.stub().returns(readStream)
378+
};
379+
380+
var block1 = {
381+
hash: '00000000050a6d07f583beba2d803296eb1e9d4980c4a20f206c584e89a4f02b',
382+
timestamp: 1441911909
383+
};
384+
385+
var block2 = {
386+
hash: '000000000383752a55a0b2891ce018fd0fdc0b6352502772b034ec282b4a1bf6',
387+
timestamp: 1441913112
388+
};
389+
390+
db.getBlockHashesByTimestamp(1441914000, 1441911000, function(err, hashes) {
391+
should.not.exist(err);
392+
hashes.should.deep.equal([block2.hash, block1.hash]);
393+
done();
394+
});
395+
396+
readStream.emit('data', {
397+
key: db._encodeBlockIndexKey(block2.timestamp),
398+
value: db._encodeBlockIndexValue(block2.hash)
399+
});
400+
401+
readStream.emit('data', {
402+
key: db._encodeBlockIndexKey(block1.timestamp),
403+
value: db._encodeBlockIndexValue(block1.hash)
404+
});
405+
406+
readStream.emit('close');
407+
});
408+
409+
it('should give an error if the stream has an error', function(done) {
410+
var db = new DB(baseConfig);
411+
var readStream = new EventEmitter();
412+
db.store = {
413+
createReadStream: sinon.stub().returns(readStream)
414+
};
415+
416+
db.getBlockHashesByTimestamp(1441911000, 1441914000, function(err, hashes) {
417+
should.exist(err);
418+
err.message.should.equal('error');
419+
done();
420+
});
421+
422+
readStream.emit('error', new Error('error'));
423+
424+
readStream.emit('close');
425+
});
426+
427+
it('should give an error if the timestamp is out of range', function(done) {
428+
var db = new DB(baseConfig);
429+
var readStream = new EventEmitter();
430+
db.store = {
431+
createReadStream: sinon.stub().returns(readStream)
432+
};
433+
434+
db.getBlockHashesByTimestamp(-1, -5, function(err, hashes) {
435+
should.exist(err);
436+
err.message.should.equal('Invalid Argument: timestamp out of bounds');
437+
done();
438+
});
439+
});
440+
});
441+
372442
describe('#getPrevHash', function() {
373443
it('should return prevHash from bitcoind', function(done) {
374444
var db = new DB(baseConfig);
@@ -650,10 +720,22 @@ describe('DB Service', function() {
650720
batch: sinon.stub().callsArg(1)
651721
};
652722

723+
var block = {
724+
hash: '00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863',
725+
header: {
726+
timestamp: 1441906365
727+
}
728+
};
729+
653730
it('should call blockHandler in all services and perform operations', function(done) {
654-
db.runAllBlockHandlers('block', true, function(err) {
731+
db.runAllBlockHandlers(block, true, function(err) {
655732
should.not.exist(err);
656-
db.store.batch.args[0][0].should.deep.equal(['op1', 'op2', 'op3', 'op4', 'op5']);
733+
var blockOp = {
734+
type: 'put',
735+
key: db._encodeBlockIndexKey(1441906365),
736+
value: db._encodeBlockIndexValue('00000000000000000d0aaf93e464ddeb503655a0750f8b9c6eed0bdf0ccfc863')
737+
};
738+
db.store.batch.args[0][0].should.deep.equal([blockOp, 'op1', 'op2', 'op3', 'op4', 'op5']);
657739
done();
658740
});
659741
});
@@ -663,7 +745,7 @@ describe('DB Service', function() {
663745
Service3.prototype.blockHandler = sinon.stub().callsArgWith(2, new Error('error'));
664746
db.node.services.service3 = new Service3();
665747

666-
db.runAllBlockHandlers('block', true, function(err) {
748+
db.runAllBlockHandlers(block, true, function(err) {
667749
should.exist(err);
668750
done();
669751
});
@@ -675,7 +757,7 @@ describe('DB Service', function() {
675757
service3: new Service3()
676758
};
677759

678-
db.runAllBlockHandlers('block', true, function(err) {
760+
db.runAllBlockHandlers(block, true, function(err) {
679761
should.not.exist(err);
680762
done();
681763
});
@@ -688,7 +770,7 @@ describe('DB Service', function() {
688770
};
689771

690772
(function() {
691-
db.runAllBlockHandlers('block', true, function(err) {
773+
db.runAllBlockHandlers(block, true, function(err) {
692774
should.not.exist(err);
693775
});
694776
}).should.throw('bitcore.ErrorInvalidArgument');
@@ -701,7 +783,7 @@ describe('DB Service', function() {
701783
db.node = {};
702784
db.node.services = {};
703785
var methods = db.getAPIMethods();
704-
methods.length.should.equal(5);
786+
methods.length.should.equal(6);
705787
});
706788
});
707789

0 commit comments

Comments
 (0)