add tests

This commit is contained in:
Patrick Nagurny 2015-07-16 15:53:44 -04:00
parent f9fef7a07f
commit fa572237a6
13 changed files with 3847 additions and 61 deletions

View File

@ -1,58 +0,0 @@
var async = require('async');
var d = Date.now();
var i = 0;
function fn1(next) {
console.log('fn1');
setImmediate(function() {
next(null, [])
});
}
function fn2(next) {
console.log('fn2');
setImmediate(function() {
next(null, [])
});
}
async.whilst(
function() {
return i < 100000;
},
function(callback) {
i++;
async.series(
[
fn1.bind(this),
fn2.bind(this)
],
callback
);
},
function() {
console.log('Milliseconds', Date.now() - d);
}
);
/*async.times(100000, function(n, next) {
async.series(
[
function fn1(next) {
setImmediate(function() {
next(null, []);
});
},
function fn2(next) {
setImmediate(function() {
next(null, []);
});
}
],
next
);
}, function() {
console.log('Milliseconds', Date.now() - d);
});*/

View File

@ -111,6 +111,11 @@ Node.prototype._loadDB = function(config) {
// Other modules can inherit from our DB and replace it with their own
DB = config.DB;
}
if(!config.db) {
config.db = {};
}
config.db.network = this.network;
this.db = new DB(config.db);

View File

@ -30,7 +30,9 @@
"preinstall": "./bin/build-libbitcoind",
"install": "./bin/build-bindings",
"start": "export LD_LIBRARY_PATH=`./platform/os.sh osdir` && node example",
"debug_install": "./bin/build-libbitcoind debug && ./bin/build-bindings debug"
"debug_install": "./bin/build-libbitcoind debug && ./bin/build-bindings debug",
"test": "NODE_ENV=test mocha --recursive",
"coverage": "istanbul cover _mocha -- --recursive"
},
"tags": [
"bitcoin",
@ -44,7 +46,8 @@
"chainlib": "^0.1.1",
"libbitcoinconsensus": "^0.0.3",
"errno": "^0.1.2",
"async": "1.3.0"
"async": "1.3.0",
"memdown": "^1.0.0"
},
"devDependencies": {
"benchmark": "1.0.0",
@ -52,6 +55,7 @@
"bitcore": "^0.12.12",
"chai": "^3.0.0",
"mocha": "~1.16.2",
"sinon": "^1.15.4"
"sinon": "^1.15.4",
"proxyquire": "^1.3.1"
}
}

133
test/block.unit.js Normal file
View File

@ -0,0 +1,133 @@
'use strict';
var chai = require('chai');
var should = chai.should();
var sinon = require('sinon');
var bitcore = require('bitcore');
var BN = bitcore.crypto.BN;
var BufferWriter = bitcore.encoding.BufferWriter;
var BufferReader = bitcore.encoding.BufferReader;
var bitcoindjs = require('../');
var Block = bitcoindjs.Block;
var chainData = require('./data/pow-chain.json');
describe('Bitcoin Block', function() {
describe('@constructor', function() {
it('set bits and nonce', function() {
var block = new Block(chainData[1]);
should.exist(block.bits);
block.bits.should.equal(chainData[1].bits);
should.exist(block.nonce);
block.nonce.should.equal(chainData[1].nonce);
});
});
describe('#validate', function() {
it('rejects with invalid proof of work because hash is greater', function(done) {
var block = new Block(chainData[1]);
block.validProofOfWork = sinon.stub().returns(false);
block.validate({
db: {
getBlock: sinon.stub().callsArgWith(1, null, {})
}
}, function(err) {
should.exist(err);
err.message.should.match(/Invalid proof of work/);
done();
});
});
it('rejects with unexpected bits for chain', function(done) {
var block = new Block(chainData[1]);
block.validProofOfWork = sinon.stub().returns(true);
block.validate({
db: {
getBlock: sinon.stub().callsArgWith(1, null, {})
},
getNextWorkRequired: sinon.stub().callsArgWith(1, null, 0x1d00ffff)
}, function(err) {
should.exist(err);
err.message.should.match(/Invalid proof of work, expected block bits/);
done();
});
});
});
describe('#validProofOfWork', function() {
it('returns false if block hash is greater than target from bits', function() {
var block = new Block(chainData[1]);
var target = new BN('00000000c4769616456f587f6744a779f19a1f9056431ad03d0949458ec85ac0', 'hex');
var valid = block.validProofOfWork({
getTargetFromBits: sinon.stub().returns(target)
});
valid.should.equal(false);
});
it('returns true if block hash is less than target from bits', function() {
var block = new Block(chainData[1]);
var target = new BN('f2345678c4769616456f587f6744a779f19a1f9056431ad03d0949458ec85ac0', 'hex');
var valid = block.validProofOfWork({
getTargetFromBits: sinon.stub().returns(target)
});
valid.should.equal(true);
});
});
describe('#fromBuffer', function() {
var buffer = new Buffer('010000004404c1ff5f300e5ed830b45ec9f68fbe9a0c51c4b4eaa4ce09a03ac4ddde01750000000000000000000000000000000000000000000000000000000000000000b134de547fcc071f4a020000abcdef', 'hex');
it('deserializes correctly', function() {
var block = Block.fromBuffer(buffer);
block.version.should.equal(1);
block.prevHash.should.equal('7501deddc43aa009cea4eab4c4510c9abe8ff6c95eb430d85e0e305fffc10444');
block.merkleRoot.should.equal(new Buffer(Array(32)).toString('hex'));
block.timestamp.should.be.instanceof(Date);
block.timestamp.toISOString().should.equal('2015-02-13T17:30:25.000Z');
block.bits.should.equal(520604799);
block.nonce.should.equal(586);
block.data.should.deep.equal(new Buffer('abcdef', 'hex'));
});
it('roundtrip serialization', function() {
var actual = Block.fromBuffer(buffer).toBuffer();
actual.should.deep.equal(buffer);
});
it('set null prevHash if null hash buffer', function() {
var blockBuffer = new Buffer('0100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d4d5fd834b0100007fcc071f4a020000abcdef', 'hex');
var block = Block.fromBuffer(blockBuffer);
block.hasOwnProperty('prevHash').should.equal(true);
should.equal(block.prevHash, null);
});
});
describe('#headerToBufferWriter', function() {
it('serializes correctly', function() {
var block = new Block(chainData[1]);
var bw = new BufferWriter();
block.headerToBufferWriter(bw);
bw.bufs[0].toString('hex').should.equal('01000000'); // version
BufferReader(bw.bufs[1]).readReverse().toString('hex').should.equal(chainData[1].prevHash); // prevhash
Number(bw.bufs[2].toString('hex')).should.equal(0); // merkle root
should.exist(bw.bufs[3]); // time
bw.bufs[3].length.should.equal(4);
should.exist(bw.bufs[4]); // bits
bw.bufs[4].length.should.equal(4);
should.exist(bw.bufs[5]); // nonce
bw.bufs[5].length.should.equal(4);
});
});
describe('Bitcoin Block', function() {
it('should load and serialize the Bitcoin testnet genesis block correctly', function() {
var blockBuffer = new Buffer('0100000000000000000000000000000000000000000000000000000000000000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3888a51323a9fb8aa4b1e5e4adae5494dffff001d1aa4ae180101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000', 'hex');
var block = Block.fromBuffer(blockBuffer);
block.hash.should.equal('000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943');
});
it('should load and serialize Bitcoin testnet #1 block correctly', function() {
var blockBuffer = new Buffer('0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000', 'hex');
var block = Block.fromBuffer(blockBuffer);
block.hash.should.equal('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206');
block.toBuffer().should.deep.equal(blockBuffer);
});
});
});

240
test/chain.unit.js Normal file
View File

@ -0,0 +1,240 @@
'use strict';
var chai = require('chai');
var should = chai.should();
var sinon = require('sinon');
var async = require('async');
var proxyquire = require('proxyquire');
var memdown = require('memdown');
var bitcoindjs = require('../');
var DB = bitcoindjs.DB;
var Chain = bitcoindjs.Chain;
var Block = bitcoindjs.Block;
var chainData = require('./data/testnet-blocks.json');
describe('Bitcoin Chain', function() {
describe('@constructor', function() {
it('can create a new instance with and without `new`', function() {
var chain = new Chain();
chain = Chain();
});
});
describe('#_writeBlock', function() {
it('should update hashes and call putBlock', function(done) {
var chain = new Chain();
chain.db = {
putBlock: sinon.stub().callsArg(1)
};
chain._writeBlock({hash: 'hash', prevHash: 'prevhash'}, function(err) {
should.not.exist(err);
chain.db.putBlock.callCount.should.equal(1);
chain.cache.hashes.hash.should.equal('prevhash');
done();
});
});
});
describe('#_validateBlock', function() {
it('should call the callback', function(done) {
var chain = new Chain();
chain._validateBlock('block', function(err) {
should.not.exist(err);
done();
});
});
});
describe('#getInterval', function() {
it('get default interval', function() {
var chain = new Chain();
chain.targetTimespan.toString(10).should.equal('1209600000');
chain.targetSpacing.toString(10).should.equal('600000');
chain.getDifficultyInterval().toString(10).should.equal('2016');
});
it('get custom interval', function() {
var chain = new Chain({
targetTimespan: 30 * 60 * 1000
});
chain.getDifficultyInterval().toString(10).should.equal('3');
});
});
describe('#buildGenesisBlock', function() {
it('can handle no options', function() {
var db = {
buildGenesisData: sinon.stub().returns({})
};
var chain = new Chain({db: db});
var block = chain.buildGenesisBlock();
should.exist(block);
block.should.be.instanceof(Block);
db.buildGenesisData.calledOnce.should.equal(true);
});
it('set timestamp, nonce, bits, merkleRoot and data of the genesis', function() {
var db = {
buildGenesisData: sinon.stub().returns({
merkleRoot: 'merkleRoot',
buffer: new Buffer('abcdef', 'hex')
})
};
var chain = new Chain({db: db});
var timestamp = '2015-03-20T14:46:01.118Z';
var block = chain.buildGenesisBlock({
timestamp: timestamp,
nonce: 1,
bits: 520617984
});
should.exist(block);
block.should.be.instanceof(Block);
block.timestamp.toISOString().should.equal(timestamp);
block.nonce.should.equal(1);
block.bits.should.equal(520617984);
block.merkleRoot.should.equal('merkleRoot');
block.data.should.deep.equal(new Buffer('abcdef', 'hex'));
db.buildGenesisData.calledOnce.should.equal(true);
});
});
describe('#getRetargetedBits', function() {
it('should get the correct bits', function() {
var chain = new Chain();
var bits = chain.getRetargetedBits(486604799, 12 * 24 * 60 * 60 * 1000);
bits.should.equal(484142299);
});
it('should get the correct bits if actual timespan was really small', function() {
var chain = new Chain();
var bits1 = chain.getRetargetedBits(486604799, 2 * 24 * 60 * 60 * 1000);
bits1.should.equal(473956288);
var bits2 = chain.getRetargetedBits(486604799, 1 * 24 * 60 * 60 * 1000);
bits2.should.equal(473956288);
});
it('should get the correct bits if actual timespan was really large', function() {
var chain = new Chain();
var bits1 = chain.getRetargetedBits(436567560, 60 * 24 * 60 * 60 * 1000);
bits1.should.equal(437647392);
var bits2 = chain.getRetargetedBits(436567560, 70 * 24 * 60 * 60 * 1000);
bits2.should.equal(437647392);
});
it('should not give higher than max bits', function() {
var chain = new Chain();
var bits = chain.getRetargetedBits(486604799, 16 * 24 * 60 * 60 * 1000);
bits.should.equal(486604799);
});
});
describe('#getTargetFromBits/#getBitsFromTarget', function() {
var target1;
var target2;
var target3;
it('should calculate the target correctly', function() {
var chain = new Chain();
var target1 = chain.getTargetFromBits(0x1b0404cb);
var expected = '00000000000404cb000000000000000000000000000000000000000000000000';
target1.toString('hex', 32).should.equal(expected);
});
it('should error if bits is too small', function() {
var chain = new Chain();
(function(){
var target1 = chain.getTargetFromBits(Chain.DEFAULTS.MIN_BITS - 1);
}).should.throw('bits is too small');
});
it('should error if bits is too large', function() {
var chain = new Chain();
(function(){
var target1 = chain.getTargetFromBits(Chain.DEFAULTS.MAX_BITS + 1);
}).should.throw('bits is too big');
});
it('should get the bits', function() {
var chain = new Chain();
var expected = '00000000000404cb000000000000000000000000000000000000000000000000';
var bits = chain.getBitsFromTarget(expected);
bits.should.equal(0x1b0404cb);
});
});
describe('#getDifficultyFromBits', function() {
it('should return the correct difficulty', function() {
var genesis = {bits: 0x1d00ffff};
var chain = new Chain({genesis: genesis});
var difficulty = chain.getDifficultyFromBits(0x1818bb87);
difficulty.toString(10).should.equal('44455415962');
});
});
describe('#getBlockWeight', function() {
it('should return the correct block weight for normal targets', function(done) {
var block = {bits: 0x1d00ffff};
var db = {
getBlock: sinon.stub().callsArgWith(1, null, block)
};
var chain = new Chain({db: db});
chain.getBlockWeight(block, function(err, weight) {
weight.toString(16).should.equal('100010001');
done();
});
});
it('should correctly report an error if it happens', function(done) {
var block = {bits: 0x1d00ffff};
var db = {
getBlock: sinon.stub().callsArgWith(1, new Error('fake error'))
};
var chain = new Chain({db: db});
chain.getBlockWeight(block, function(err, weight) {
should.exist(err);
err.message.should.equal('fake error');
done();
});
});
it('should correctly report an error for a null block', function(done) {
var block = {bits: 0x1d00ffff};
var db = {
getBlock: sinon.stub().callsArgWith(1, null, null)
};
var chain = new Chain({db: db});
chain.getBlockWeight(block, function(err, weight) {
should.exist(err);
err.message.should.match(/Block not found/);
done();
});
});
});
describe('Bitcoin POW', function() {
it('should calculate correct difficulty for block 201600', function(done) {
var chain = new Chain();
var beginBlock = {
timestamp: new Date(1348092851000),
bits: 436591499
};
var lastBlock = {
timestamp: new Date(1349227021000),
bits: 436591499
};
chain.getHeightForBlock = sinon.stub().callsArgWith(1, null, 201599);
chain.getBlockAtHeight = sinon.stub().callsArgWith(2, null, beginBlock);
chain.getNextWorkRequired(lastBlock, function(err, bits) {
should.not.exist(err);
bits.should.equal(436567560);
done();
});
});
});
});

View File

@ -0,0 +1,14 @@
[
{
"comment": "sends to tx[1]",
"hex":"0100000001dee8f4266e83072e0ad258125cc5a42ac25d2d2c73e6e2e873413b3939af1605000000006b483045022100ae987d056f81d2c982b71b0406f2374c1958b24bd289d77371347e275d2a62c002205148b17173be18af4e1e73ce2b0fd600734ea77087754bdba5dc7d645b01880a01210226ab3b46f85bf32f63778c680e16ef8b3fcb51d638a7980d651bfaeae6c17752ffffffff0170820300000000001976a9142baf68e3681df183375a4f4c10306de9a5c6cc7788ac00000000"
},
{
"comment": "spends from tx[0] (valid)",
"hex":"0100000001f77c71cf8c272d22471f054cae7fb48561ebcf004b8ec8f9f65cd87af82a2944000000006a47304402203c2bc91a170facdc5ef4b5b94c413bc7a10f65e09b326d205f070b17aa94d67102205b684111af2a20171eb65db73e6c73f9e77e6e6f739e050bc052ed6ecc9feb4a01210365d8756a4f3fc738105cfab8d80a85189bdb4db5af83374e645b79e2aadd976effffffff01605b0300000000001976a9149e84d1295471958e5ffccd8d36a57bd5d220f8ed88ac00000000"
},
{
"comment": "spends from tx[0] (missing signature)",
"hex":"0100000001f77c71cf8c272d22471f054cae7fb48561ebcf004b8ec8f9f65cd87af82a29440000000000ffffffff01605b0300000000001976a9149e84d1295471958e5ffccd8d36a57bd5d220f8ed88ac00000000"
}
]

File diff suppressed because one or more lines are too long

58
test/data/pow-chain.json Normal file
View File

@ -0,0 +1,58 @@
[
{
"version": 1,
"prevHash": null,
"timestamp": "2015-04-16T20:02:24.777Z",
"bits": 520617984,
"nonce": 0,
"data": ""
},
{
"version": 1,
"prevHash": "bab3003201bdf327ac03735e70a5f02968bc1e8cf74cc9045ae960c074139386",
"timestamp": "2015-04-16T20:02:26.650Z",
"bits": 520617984,
"nonce": 6256,
"data": ""
},
{
"version": 1,
"prevHash": "0002cbf2807997765971f14bdd7c748e93c315c2d3af35b85c6604126c788fa8",
"timestamp": "2015-04-16T20:02:27.885Z",
"bits": 520617984,
"nonce": 12232,
"data": ""
},
{
"version": 1,
"prevHash": "0007b3fec55496a3741caa992aac55395921a965e8cec6192659f266eec39f62",
"timestamp": "2015-04-16T20:02:28.559Z",
"bits": 520355840,
"nonce": 6492,
"data": ""
},
{
"version": 1,
"prevHash": "000002de06d8fdd4d7036fc99ddc8c9b432bfa910e0968756b988e25f7f43d8e",
"timestamp": "2015-04-16T20:02:29.593Z",
"bits": 520355840,
"nonce": 9502,
"data": ""
},
{
"version": 1,
"prevHash": "00016a5c727ef18b406b16c188af017c101f5a32c2d881e92dd7aa1746b185ea",
"timestamp": "2015-04-16T20:02:30.713Z",
"bits": 520355840,
"nonce": 10586,
"data": ""
},
{
"version": 1,
"prevHash": "00023ac90a33ce9faf07f407b525f186180c902fbb2e71c48cd73ffb25a8dc5b",
"timestamp": "22015-04-16T20:02:31.440Z",
"bits": 520181077,
"nonce": 6983,
"data": ""
}
]

File diff suppressed because one or more lines are too long

61
test/data/wallet.json Normal file
View File

@ -0,0 +1,61 @@
{
"xprivkey": "tprv8ZgxMBicQKsPeHnrBXa8NfcpBQExTXq9gz8ijFPx6EJonZ5xiMt4kqWYRPJb5AmvNkTxV5qMTzkCUUfC4s3MQhpBcLg9AtpCAg5mqMeRqVr",
"utxos": {
"mv7aNQh6soUFysgh1Ax82BKBTyf1V4qWha": [
{
"txId" : "a0a08e397203df68392ee95b3f08b0b3b3e2401410a38d46ae0874f74846f2e9",
"outputIndex" : 0,
"address" : "mv7aNQh6soUFysgh1Ax82BKBTyf1V4qWha",
"script" : "76a914a01e048440b4cc651df6bdc41ec47781788b4b6d88ac",
"satoshis" : 70000
}
],
"mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L": [
{
"txId" : "b0a08e397203df68392ee95b3f08b0b3b3e2401410a38d46ae0874f74846f2e9",
"outputIndex" : 1,
"address" : "mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L",
"script" : "76a914089acaba6af8b2b4fb4bed3b747ab1e4e60b496588ac",
"satoshis" : 30000
}
]
},
"utxosAmount": {
"mv7aNQh6soUFysgh1Ax82BKBTyf1V4qWha": [
{
"txId" : "a0a08e397203df68392ee95b3f08b0b3b3e2401410a38d46ae0874f74846f2e9",
"outputIndex" : 0,
"address" : "mv7aNQh6soUFysgh1Ax82BKBTyf1V4qWha",
"script" : "76a914089acaba6af8b2b4fb4bed3b747ab1e4e60b496588ac",
"amount" : 0.0007
}
],
"mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L": [
{
"txId" : "b0a08e397203df68392ee95b3f08b0b3b3e2401410a38d46ae0874f74846f2e9",
"outputIndex" : 1,
"address" : "mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L",
"script" : "76a914089acaba6af8b2b4fb4bed3b747ab1e4e60b496588ac",
"amount" : 0.0003
}
]
},
"utxosNaN": {
"mv7aNQh6soUFysgh1Ax82BKBTyf1V4qWha": [
{
"txId" : "a0a08e397203df68392ee95b3f08b0b3b3e2401410a38d46ae0874f74846f2e9",
"outputIndex" : 0,
"address" : "mv7aNQh6soUFysgh1Ax82BKBTyf1V4qWha",
"script" : "76a914089acaba6af8b2b4fb4bed3b747ab1e4e60b496588ac"
}
],
"mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L": [
{
"txId" : "b0a08e397203df68392ee95b3f08b0b3b3e2401410a38d46ae0874f74846f2e9",
"outputIndex" : 1,
"address" : "mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L",
"script" : "76a914089acaba6af8b2b4fb4bed3b747ab1e4e60b496588ac"
}
]
}
}

597
test/db.unit.js Normal file
View File

@ -0,0 +1,597 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var chainlib = require('chainlib');
var levelup = chainlib.deps.levelup;
var bitcoindjs = require('../');
var DB = bitcoindjs.DB;
var blockData = require('./data/livenet-345003.json');
var bitcore = require('bitcore');
var EventEmitter = require('events').EventEmitter;
var errors = bitcoindjs.errors;
var memdown = require('memdown');
describe('Bitcoin DB', function() {
var coinbaseAmount = 50 * 1e8;
describe('#getBlock', function() {
var db = new DB({store: memdown});
db.bitcoind = {
getBlock: sinon.stub().callsArgWith(1, null, new Buffer(blockData, 'hex'))
};
db.Block = {
fromBuffer: sinon.stub().returns('block')
};
it('should get the block from bitcoind.js', function(done) {
db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) {
should.not.exist(err);
block.should.equal('block');
done();
});
});
it('should give an error when bitcoind.js gives an error', function(done) {
db.bitcoind.getBlock = sinon.stub().callsArgWith(1, new Error('error'));
db.getBlock('00000000000000000593b60d8b4f40fd1ec080bdb0817d475dae47b5f5b1f735', function(err, block) {
should.exist(err);
err.message.should.equal('error');
done();
});
});
});
describe('#putBlock', function() {
it('should call _updatePrevHashIndex', function(done) {
var db = new DB({store: memdown});
db._updatePrevHashIndex = sinon.stub().callsArg(1);
db.putBlock('block', function(err) {
should.not.exist(err);
db._updatePrevHashIndex.called.should.equal(true);
done();
});
});
});
describe('#buildGenesisData', function() {
it('build genisis data', function() {
var db = new DB({path: 'path', store: memdown});
db.buildCoinbaseTransaction = sinon.stub().returns({
toBuffer: sinon.stub().returns(new Buffer('abcdef', 'hex'))
});
db.getMerkleRoot = sinon.stub().returns('merkleRoot');
var data = db.buildGenesisData();
data.buffer.should.deep.equal(new Buffer('01abcdef', 'hex'));
data.merkleRoot.should.equal('merkleRoot');
});
});
describe('#buildCoinbaseTransaction', function() {
it('should correctly build a coinbase transaction with no fees', function() {
var db = new DB({path: 'path', store: memdown});
db.wallet = {
getAddress: sinon.stub().returns('mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L')
};
db.coinbaseAmount = coinbaseAmount;
var coinbaseTx = db.buildCoinbaseTransaction();
coinbaseTx.inputs.length.should.equal(1);
var input = coinbaseTx.inputs[0];
var expectedTxId = '0000000000000000000000000000000000000000000000000000000000000000';
input.prevTxId.toString('hex').should.equal(expectedTxId);
should.exist(input.outputIndex);
should.exist(input.sequenceNumber);
should.exist(input._script); // coinbase input script returns null
coinbaseTx.outputs.length.should.equal(1);
var output = coinbaseTx.outputs[0];
output.satoshis.should.equal(coinbaseAmount);
});
it('should correctly build a coinbase transaction with fees', function() {
var db = new DB({path: 'path', store: memdown});
db.wallet = {
getAddress: sinon.stub().returns('mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L')
};
db.coinbaseAmount = coinbaseAmount;
var transactions = [
{
_getInputAmount: sinon.stub().returns(5000),
_getOutputAmount: sinon.stub().returns(4000),
isCoinbase: sinon.stub().returns(false)
},
{
_getInputAmount: sinon.stub().returns(8000),
_getOutputAmount: sinon.stub().returns(7000),
isCoinbase: sinon.stub().returns(false)
}
];
var coinbaseTx = db.buildCoinbaseTransaction(transactions);
coinbaseTx.inputs.length.should.equal(1);
var input = coinbaseTx.inputs[0];
var expectedTxId = '0000000000000000000000000000000000000000000000000000000000000000';
input.prevTxId.toString('hex').should.equal(expectedTxId);
should.exist(input.outputIndex);
should.exist(input.sequenceNumber);
should.exist(input._script); // coinbase input returns null
coinbaseTx.outputs.length.should.equal(1);
var output = coinbaseTx.outputs[0];
output.satoshis.should.equal(coinbaseAmount + 2000);
});
it('should throw an error if wallet not included', function() {
var db = new DB({path: 'path', store: memdown});
(function() {
db.buildCoinbaseTransaction();
}).should.throw('Wallet required to build coinbase');
});
it('will build a coinbase database with different data', function() {
var db = new DB({path: 'path', store: memdown});
db.wallet = {
getAddress: sinon.stub().returns('mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L')
};
var tx1 = db.buildCoinbaseTransaction().uncheckedSerialize();
var tx2 = db.buildCoinbaseTransaction().uncheckedSerialize();
tx1.should.not.equal(tx2);
});
it('can pass in custom data', function() {
var db = new DB({path: 'path', store: memdown});
db.wallet = {
getAddress: sinon.stub().returns('mzso6uXxfDCq4L6xAffUD9BPWo6bdFBZ2L')
};
var tx1 = db.buildCoinbaseTransaction(null, new Buffer('abcdef', 'hex'));
var data = tx1.inputs[0]._script.getData();
data.should.deep.equal(new Buffer('abcdef', 'hex'));
});
});
describe('#getOutputTotal', function() {
it('should return the correct value including the coinbase', function() {
var totals = [10, 20, 30];
var db = new DB({path: 'path', store: memdown});
var transactions = totals.map(function(total) {
return {
_getOutputAmount: function() {
return total;
},
isCoinbase: function() {
return total === 10 ? true : false;
}
};
});
var grandTotal = db.getOutputTotal(transactions);
grandTotal.should.equal(60);
});
it('should return the correct value excluding the coinbase', function() {
var totals = [10, 20, 30];
var db = new DB({path: 'path', store: memdown});
var transactions = totals.map(function(total) {
return {
_getOutputAmount: function() {
return total;
},
isCoinbase: function() {
return total === 10 ? true : false;
}
};
});
var grandTotal = db.getOutputTotal(transactions, true);
grandTotal.should.equal(50)
});
});
describe('#getInputTotal', function() {
it('should return the correct value', function() {
var totals = [10, 20, 30];
var db = new DB({path: 'path', store: memdown});
var transactions = totals.map(function(total) {
return {
_getInputAmount: function() {
return total;
},
isCoinbase: sinon.stub().returns(false)
};
});
var grandTotal = db.getInputTotal(transactions);
grandTotal.should.equal(60);
});
it('should return 0 if the tx is a coinbase', function() {
var db = new DB({store: memdown});
var tx = {
isCoinbase: sinon.stub().returns(true)
};
var total = db.getInputTotal([tx]);
total.should.equal(0);
});
});
describe('#_updateOutputs', function() {
var block = bitcore.Block.fromString(blockData);
var db = new DB({path: 'path', store: memdown, network: 'livenet'});
db.getTransactionsFromBlock = function() {
return block.transactions.slice(0, 8);
};
var data = [
{
key: {
address: '1F1MAvhTKg2VG29w8cXsiSN2PJ8gSsrJw',
timestamp: 1424836934000,
txid: 'fdbefe0d064729d85556bd3ab13c3a889b685d042499c02b4aa2064fb1e16923',
outputIndex: 0
},
value: {
satoshis: 2502227470,
script: 'OP_DUP OP_HASH160 20 0x02a61d2066d19e9e2fd348a8320b7ebd4dd3ca2b OP_EQUALVERIFY OP_CHECKSIG',
blockHeight: 345003
}
},
{
key: {
prevTxId: '3d7d5d98df753ef2a4f82438513c509e3b11f3e738e94a7234967b03a03123a9',
prevOutputIndex: 32
},
value: {
txid: '5780f3ee54889a0717152a01abee9a32cec1b0cdf8d5537a08c7bd9eeb6bfbca',
inputIndex: 0,
timestamp: 1424836934000
}
},
{
key: {
address: '1Ep5LA4T6Y7zaBPiwruUJurjGFvCJHzJhm',
timestamp: 1424836934000,
txid: 'e66f3b989c790178de2fc1a5329f94c0d8905d0d3df4e7ecf0115e7f90a6283d',
outputIndex: 1
},
value: {
satoshis: 3100000,
script: 'OP_DUP OP_HASH160 20 0x9780ccd5356e2acc0ee439ee04e0fe69426c7528 OP_EQUALVERIFY OP_CHECKSIG',
blockHeight: 345003
}
}
];
var key0 = data[0].key;
var value0 = data[0].value;
var key3 = data[1].key;
var value3 = data[1].value;
var key64 = data[2].key;
var value64 = data[2].value;
it('should create the correct operations when updating/adding outputs', function(done) {
db._updateOutputs({height: 345003, timestamp: new Date(1424836934000)}, true, function(err, operations) {
should.not.exist(err);
operations.length.should.equal(81);
operations[0].type.should.equal('put');
var expected0 = ['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-');
operations[0].key.should.equal(expected0);
operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':'));
operations[3].type.should.equal('put');
var expected3 = ['sp', key3.prevTxId, key3.prevOutputIndex].join('-');
operations[3].key.should.equal(expected3);
operations[3].value.should.equal([value3.txid, value3.inputIndex, value3.timestamp].join(':'));
operations[64].type.should.equal('put');
var expected64 = ['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-');
operations[64].key.should.equal(expected64);
operations[64].value.should.equal([value64.satoshis, value64.script, value64.blockHeight].join(':'));
done();
});
});
it('should create the correct operations when removing outputs', function(done) {
db._updateOutputs({height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) {
should.not.exist(err);
operations.length.should.equal(81);
operations[0].type.should.equal('del');
operations[0].key.should.equal(['outs', key0.address, key0.timestamp, key0.txid, key0.outputIndex].join('-'));
operations[0].value.should.equal([value0.satoshis, value0.script, value0.blockHeight].join(':'));
operations[3].type.should.equal('del');
operations[3].key.should.equal(['sp', key3.prevTxId, key3.prevOutputIndex].join('-'));
operations[3].value.should.equal([value3.txid, value3.inputIndex, value3.timestamp].join(':'));
operations[64].type.should.equal('del');
operations[64].key.should.equal(['outs', key64.address, key64.timestamp, key64.txid, key64.outputIndex].join('-'));
operations[64].value.should.equal([value64.satoshis, value64.script, value64.blockHeight].join(':'));
done();
});
});
it('should continue if output script is null', function(done) {
var db = new DB({path: 'path', store: memdown, network: 'livenet'});
var transactions = [
{
inputs: [],
outputs: [
{
script: null,
satoshis: 1000,
}
],
isCoinbase: sinon.stub().returns(false)
}
];
db.getTransactionsFromBlock = function() {
return transactions;
};
db._updateOutputs({height: 345003, timestamp: new Date(1424836934000)}, false, function(err, operations) {
should.not.exist(err);
operations.length.should.equal(0);
done();
});
});
});
describe('#_onChainAddBlock', function() {
var db = new DB({path: 'path', store: memdown});
db._updateOutputs = sinon.stub().callsArgWith(2, null, ['1a', '1b']);
db.store = {
batch: sinon.stub().callsArg(1)
};
it('should give error when there is a failure to write', function() {
var errordb = new DB({path: 'path', store: memdown});
errordb._updateOutputs = sinon.stub().callsArgWith(2, null, ['1a', '1b']);
errordb.store = {
batch: sinon.stub().callsArgWith(1, new Error('error'))
};
errordb._onChainAddBlock('block', function(err) {
should.exist(err);
});
});
it('should call block processing functions and write to database', function(done) {
db._onChainAddBlock('block', function(err) {
should.not.exist(err);
db._updateOutputs.calledOnce.should.equal(true);
db._updateOutputs.calledWith('block', true).should.equal(true);
db.store.batch.args[0][0].should.deep.equal(['1a', '1b']);
done();
});
});
it('should halt on an error and not write to database', function(done) {
db._updateOutputs.reset();
db.store.batch.reset();
db._updateOutputs = sinon.stub().callsArgWith(2, new Error('error'));
db._onChainAddBlock('block', function(err) {
should.exist(err);
err.message.should.equal('error');
db._updateOutputs.calledOnce.should.equal(true);
db._updateOutputs.calledWith('block', true).should.equal(true);
db.store.batch.called.should.equal(false);
done();
});
});
});
describe('#_onChainRemoveBlock', function() {
var db = new DB({path: 'path', store: memdown});
db._updateOutputs = sinon.stub().callsArgWith(2, null, ['1a', '1b']);
db.store = {
batch: sinon.stub().callsArg(1)
};
it('should give error when there is a failure to write', function() {
var errordb = new DB({path: 'path', store: memdown});
errordb._updateOutputs = sinon.stub().callsArgWith(2, null, ['1a', '1b']);
errordb.store = {
batch: sinon.stub().callsArgWith(1, new Error('error'))
};
errordb._onChainRemoveBlock('block', function(err) {
should.exist(err);
});
});
it('should call block processing functions and write to database', function(done) {
db._onChainRemoveBlock('block', function(err) {
should.not.exist(err);
db._updateOutputs.calledOnce.should.equal(true);
db._updateOutputs.calledWith('block', false).should.equal(true);
db.store.batch.args[0][0].should.deep.equal(['1a', '1b']);
done();
});
});
it('should halt on an error and not write to database', function(done) {
db._updateOutputs.reset();
db.store.batch.reset();
db._updateOutputs = sinon.stub().callsArgWith(2, new Error('error'));
db._onChainRemoveBlock('block', function(err) {
should.exist(err);
err.message.should.equal('error');
db._updateOutputs.calledOnce.should.equal(true);
db._updateOutputs.calledWith('block', false).should.equal(true);
db.store.batch.called.should.equal(false);
done();
});
});
});
describe('#getAPIMethods', function() {
it('should return the correct methods', function() {
var db = new DB({path: 'path', store: memdown});
var methods = db.getAPIMethods();
methods.length.should.equal(6);
});
});
describe('#getBalance', function() {
it('should sum up the unspent outputs', function(done) {
var db = new DB({path: 'path', store: memdown});
var outputs = [
{satoshis: 1000}, {satoshis: 2000}, {satoshis: 3000}
];
db.getUnspentOutputs = sinon.stub().callsArgWith(2, null, outputs);
db.getBalance('1DzjESe6SLmAKVPLFMj6Sx1sWki3qt5i8N', false, function(err, balance) {
should.not.exist(err);
balance.should.equal(6000);
done();
});
});
it('will handle error from unspent outputs', function(done) {
var db = new DB({path: 'path', store: memdown});
db.getUnspentOutputs = sinon.stub().callsArgWith(2, new Error('error'));
db.getBalance('someaddress', false, function(err) {
should.exist(err);
err.message.should.equal('error');
done();
});
});
});
describe('#getOutputs', function() {
var db = new DB({path: 'path', store: memdown});
var address = '1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W';
it('should get outputs for an address', function(done) {
var readStream1 = new EventEmitter();
db.store = {
createReadStream: sinon.stub().returns(readStream1)
};
db.getOutputs(address, true, function(err, outputs) {
should.not.exist(err);
outputs.length.should.equal(2);
outputs[0].address.should.equal(address);
outputs[0].txid.should.equal('125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87');
outputs[0].outputIndex.should.equal(1);
outputs[0].satoshis.should.equal(4527773864);
outputs[0].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG');
outputs[0].blockHeight.should.equal(345000);
outputs[1].address.should.equal(address);
outputs[1].txid.should.equal('3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7');
outputs[1].outputIndex.should.equal(2);
outputs[1].satoshis.should.equal(10000);
outputs[1].script.should.equal('OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG');
outputs[1].blockHeight.should.equal(345004);
done();
});
var data1 = {
key: ['outs', address, '1424835319000', '125dd0e50fc732d67c37b6c56be7f9dc00b6859cebf982ee2cc83ed2d604bf87', '1'].join('-'),
value: ['4527773864', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345000'].join(':')
};
var data2 = {
key: ['outs', address, '1424837300000', '3b6bc2939d1a70ce04bc4f619ee32608fbff5e565c1f9b02e4eaa97959c59ae7', '2'].join('-'),
value: ['10000', 'OP_DUP OP_HASH160 038a213afdfc551fc658e9a2a58a86e98d69b687 OP_EQUALVERIFY OP_CHECKSIG', '345004'].join(':')
};
readStream1.emit('data', data1);
readStream1.emit('data', data2);
readStream1.emit('close');
});
it('should give an error if the readstream has an error', function(done) {
var readStream2 = new EventEmitter();
db.store = {
createReadStream: sinon.stub().returns(readStream2)
};
db.getOutputs(address, true, function(err, outputs) {
should.exist(err);
err.message.should.equal('readstreamerror');
done();
});
readStream2.emit('error', new Error('readstreamerror'));
process.nextTick(function() {
readStream2.emit('close');
});
});
});
describe('#getUnspentOutputs', function() {
it('should filter out spent outputs', function(done) {
var outputs = [
{
satoshis: 1000,
spent: false,
},
{
satoshis: 2000,
spent: true
},
{
satoshis: 3000,
spent: false
}
];
var i = 0;
var db = new DB({path: 'path', store: memdown});
db.getOutputs = sinon.stub().callsArgWith(2, null, outputs);
db.isUnspent = function(output, queryMempool, callback) {
callback(!outputs[i].spent);
i++;
};
db.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) {
should.not.exist(err);
outputs.length.should.equal(2);
outputs[0].satoshis.should.equal(1000);
outputs[1].satoshis.should.equal(3000);
done();
});
});
it('should handle an error from getOutputs', function(done) {
var db = new DB({path: 'path', store: memdown});
db.getOutputs = sinon.stub().callsArgWith(2, new Error('error'));
db.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) {
should.exist(err);
err.message.should.equal('error');
done();
});
});
it('should handle when there are no outputs', function(done) {
var db = new DB({path: 'path', store: memdown});
db.getOutputs = sinon.stub().callsArgWith(2, null, []);
db.getUnspentOutputs('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(err, outputs) {
should.exist(err);
err.should.be.instanceof(errors.NoOutputs);
outputs.length.should.equal(0);
done();
});
});
});
describe('#isUnspent', function() {
var db = new DB({path: 'path', store: memdown});
it('should give true when isSpent() gives false', function(done) {
db.isSpent = sinon.stub().callsArgWith(2, false);
db.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) {
unspent.should.equal(true);
done();
});
});
it('should give false when isSpent() gives true', function(done) {
db.isSpent = sinon.stub().callsArgWith(2, true);
db.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) {
unspent.should.equal(false);
done();
});
});
it('should give false when isSpent() returns an error', function(done) {
db.isSpent = sinon.stub().callsArgWith(2, new Error('error'));
db.isUnspent('1KiW1A4dx1oRgLHtDtBjcunUGkYtFgZ1W', false, function(unspent) {
unspent.should.equal(false);
done();
});
});
});
describe('#isSpent', function() {
var db = new DB({path: 'path', store: memdown});
db.bitcoind = {
isSpent: sinon.stub().returns(true)
};
it('should give true if bitcoind.isSpent gives true', function(done) {
db.isSpent('output', true, function(spent) {
spent.should.equal(true);
done();
});
});
});
});

412
test/node.unit.js Normal file
View File

@ -0,0 +1,412 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var bitcore = require('bitcore');
var Networks = bitcore.Networks;
var blockData = require('./data/livenet-345003.json');
var Block = require('../lib/block');
var proxyquire = require('proxyquire');
var chainlib = require('chainlib');
var OriginalNode = chainlib.Node;
var BaseNode = function() {};
util.inherits(BaseNode, EventEmitter);
BaseNode.log = chainlib.log;
BaseNode.prototype._loadConfiguration = sinon.spy();
BaseNode.prototype._initialize = sinon.spy();
chainlib.Node = BaseNode;
var Node = proxyquire('../lib/node', {chainlib: chainlib});
chainlib.Node = OriginalNode;
describe('Bitcoind Node', function() {
describe('#_loadConfiguration', function() {
it('should call the necessary methods', function() {
var node = new Node({});
node._loadBitcoind = sinon.spy();
node._loadConfiguration({});
node._loadBitcoind.called.should.equal(true);
BaseNode.prototype._loadConfiguration.called.should.equal(true);
});
});
describe('#setSyncStrategy', function() {
it('will call p2p.startSync', function() {
var node = new Node({});
node.p2p = {
startSync: sinon.spy()
};
node.setSyncStrategy(Node.SYNC_STRATEGIES.P2P);
node.p2p.startSync.callCount.should.equal(1);
});
it('will call this._syncBitcoind and disable p2p sync', function() {
var node = new Node({});
node.p2p = {};
node._syncBitcoind = sinon.spy();
node.setSyncStrategy(Node.SYNC_STRATEGIES.BITCOIND);
node._syncBitcoind.callCount.should.equal(1);
node.p2p.disableSync.should.equal(true);
});
it('will error with an unknown strategy', function() {
var node = new Node({});
(function(){
node.setSyncStrategy('unknown');
}).should.throw('Strategy "unknown" is unknown');
});
});
describe('#_loadBitcoind', function() {
it('should initialize', function() {
var node = new Node({});
node._loadBitcoind({});
should.exist(node.bitcoind);
});
it('should initialize with testnet', function() {
var node = new Node({});
node._loadBitcoind({testnet: true});
should.exist(node.bitcoind);
});
});
describe('#_syncBitcoind', function() {
it('will get and add block up to the tip height', function(done) {
var node = new Node({});
node.p2p = {
synced: false
};
node.Block = Block;
node.syncStrategy = Node.SYNC_STRATEGIES.BITCOIND;
node.setSyncStrategy = sinon.stub();
node.bitcoind = {
getInfo: sinon.stub().returns({blocks: 2}),
getBlock: sinon.stub().callsArgWith(1, null, new Buffer(blockData))
};
node.chain = {
tip: {
__height: 0
},
addBlock: function(block, callback) {
node.chain.tip.__height += 1;
callback();
}
};
node.on('synced', function() {
node.p2p.synced.should.equal(true);
node.setSyncStrategy.callCount.should.equal(1);
done();
});
node._syncBitcoind();
});
it('will exit and emit error with error from bitcoind.getBlock', function(done) {
var node = new Node({});
node.p2p = {
synced: false
};
node.syncStrategy = Node.SYNC_STRATEGIES.BITCOIND;
node.setSyncStrategy = sinon.stub();
node.bitcoind = {
getInfo: sinon.stub().returns({blocks: 2}),
getBlock: sinon.stub().callsArgWith(1, new Error('test error'))
};
node.chain = {
tip: {
__height: 0
}
};
node.on('error', function(err) {
err.message.should.equal('test error');
done();
});
node._syncBitcoind();
});
it('will exit if sync strategy is changed to bitcoind', function(done) {
var node = new Node({});
node.p2p = {
synced: false
};
node.syncStrategy = Node.SYNC_STRATEGIES.P2P;
node.setSyncStrategy = sinon.stub();
node.bitcoind = {
getInfo: sinon.stub().returns({blocks: 2})
};
node.chain = {
tip: {
__height: 0
}
};
node.on('synced', function() {
node.p2p.synced.should.equal(true);
node.setSyncStrategy.callCount.should.equal(1);
done();
});
node._syncBitcoind();
});
});
describe('#_loadNetwork', function() {
it('should add the network that was listed in the config', function() {
var config = {
network: {
name: 'chainlib',
alias: 'chainlib',
pubkeyhash: 0x1c,
privatekey: 0x1e,
scripthash: 0x28,
xpubkey: 0x02e8de8f,
xprivkey: 0x02e8da54,
networkMagic: 0x0c110907,
port: 9333
}
};
var node = new Node(config);
node._loadNetwork(config);
var network = Networks.get('chainlib');
should.exist(network);
node.network.name.should.equal('chainlib');
});
it('should use the testnet network if testnet is specified', function() {
var config = {
testnet: true
};
var node = new Node(config);
node._loadNetwork(config);
node.network.name.should.equal('testnet');
});
it('should use the livenet network if nothing is specified', function() {
var config = {};
var node = new Node(config);
node._loadNetwork(config);
node.network.name.should.equal('livenet');
});
});
describe('#_loadDB', function() {
it('should load the db', function() {
var DB = function() {};
var config = {
DB: DB
};
var node = new Node(config);
node._loadDB(config);
node.db.should.be.instanceof(DB);
});
});
describe('#_loadP2P', function() {
it('should load p2p', function() {
var config = {};
var node = new Node(config);
node.db = {
Transaction: bitcore.Transaction
};
node.network = Networks.get('testnet');
node._loadP2P(config);
should.exist(node.p2p);
node.p2p.noListen.should.equal(true);
node.p2p.pool.network.should.deep.equal(node.network);
node.db.Transaction.should.equal(bitcore.Transaction);
});
});
describe('#_loadConsensus', function() {
var node = new Node({});
it('should use the genesis specified in the config', function() {
var config = {
genesis: '0100000043497fd7f826957108f4a30fd9cec3aeba79972084e90ead01ea330900000000bac8b0fa927c0ac8234287e33c5f74d38d354820e24756ad709d7038fc5f31f020e7494dffff001d03e4b6720101000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0420e7494d017f062f503253482fffffffff0100f2052a010000002321021aeaf2f8638a129a3156fbe7e5ef635226b0bafd495ff03afe2c843d7e3a4b51ac00000000'
};
node._loadConsensus(config);
should.exist(node.chain);
node.chain.genesis.hash.should.equal('00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206');
});
it('should use the testnet genesis if testnet is specified', function() {
var config = {
testnet: true
};
node._loadConsensus(config);
should.exist(node.chain);
node.chain.genesis.hash.should.equal('000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943');
});
it('should use the livenet genesis if nothing is specified', function() {
var config = {};
node._loadConsensus(config);
should.exist(node.chain);
node.chain.genesis.hash.should.equal('000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f');
});
});
describe('#_initializeBitcoind', function() {
it('will call db.initialize() on ready event', function(done) {
var node = new Node({});
node.bitcoind = new EventEmitter();
node.db = {
initialize: sinon.spy()
};
sinon.stub(chainlib.log, 'info');
node.bitcoind.on('ready', function() {
setImmediate(function() {
chainlib.log.info.callCount.should.equal(1);
chainlib.log.info.restore();
node.db.initialize.callCount.should.equal(1);
done();
});
});
node._initializeBitcoind();
node.bitcoind.emit('ready');
});
it('will call emit an error from bitcoind.js', function(done) {
var node = new Node({});
node.bitcoind = new EventEmitter();
node.on('error', function(err) {
should.exist(err);
err.message.should.equal('test error');
done();
});
node._initializeBitcoind();
node.bitcoind.emit('error', new Error('test error'));
});
});
describe('#_initializeDatabase', function() {
it('will call chain.initialize() on ready event', function(done) {
var node = new Node({});
node.db = new EventEmitter();
node.chain = {
initialize: sinon.spy()
};
sinon.stub(chainlib.log, 'info');
node.db.on('ready', function() {
setImmediate(function() {
chainlib.log.info.callCount.should.equal(1);
chainlib.log.info.restore();
node.chain.initialize.callCount.should.equal(1);
done();
});
});
node._initializeDatabase();
node.db.emit('ready');
});
it('will call emit an error from db', function(done) {
var node = new Node({});
node.db = new EventEmitter();
node.on('error', function(err) {
should.exist(err);
err.message.should.equal('test error');
done();
});
node._initializeDatabase();
node.db.emit('error', new Error('test error'));
});
});
describe('#_initializeChain', function() {
it('will call p2p.initialize() on ready event', function(done) {
var node = new Node({});
node.chain = new EventEmitter();
node.p2p = {
initialize: sinon.spy()
};
sinon.stub(chainlib.log, 'info');
node.chain.on('ready', function() {
setImmediate(function() {
chainlib.log.info.callCount.should.equal(1);
chainlib.log.info.restore();
node.p2p.initialize.callCount.should.equal(1);
done();
});
});
node._initializeChain();
node.chain.emit('ready');
});
it('will call emit an error from chain', function(done) {
var node = new Node({});
node.chain = new EventEmitter();
node.on('error', function(err) {
should.exist(err);
err.message.should.equal('test error');
done();
});
node._initializeChain();
node.chain.emit('error', new Error('test error'));
});
});
describe('#_initializeP2P', function() {
it('will emit node "ready" when p2p is ready', function(done) {
var node = new Node({});
node.p2p = new EventEmitter();
sinon.stub(chainlib.log, 'info');
node.on('ready', function() {
chainlib.log.info.callCount.should.equal(1);
chainlib.log.info.restore();
done();
});
node._initializeP2P();
node.p2p.emit('ready');
});
it('will call emit an error from p2p', function(done) {
var node = new Node({});
node.p2p = new EventEmitter();
node.on('error', function(err) {
should.exist(err);
err.message.should.equal('test error');
done();
});
node._initializeP2P();
node.p2p.emit('error', new Error('test error'));
});
it('will relay synced event from p2p to node', function(done) {
var node = new Node({});
node.p2p = new EventEmitter();
node.on('synced', function() {
done();
});
node._initializeP2P();
node.p2p.emit('synced');
});
});
describe('#_initialize', function() {
it('should initialize', function(done) {
var node = new Node({});
node.chain = {};
node.Block = 'Block';
node.bitcoind = 'bitcoind';
node.p2p = {};
node.db = {};
node._initializeBitcoind = sinon.spy();
node._initializeDatabase = sinon.spy();
node._initializeChain = sinon.spy();
node._initializeP2P = sinon.spy();
node._initialize();
// references
node.db.chain.should.equal(node.chain);
node.db.Block.should.equal(node.Block);
node.db.bitcoind.should.equal(node.bitcoind);
node.chain.db.should.equal(node.db);
node.chain.p2p.should.equal(node.p2p);
node.chain.db.should.equal(node.db);
node.p2p.db.should.equal(node.db);
node.p2p.chain.should.equal(node.chain);
// events
node._initializeBitcoind.callCount.should.equal(1);
node._initializeDatabase.callCount.should.equal(1);
node._initializeChain.callCount.should.equal(1);
node._initializeP2P.callCount.should.equal(1);
// start syncing
node.setSyncStrategy = sinon.spy();
node.on('ready', function() {
node.setSyncStrategy.callCount.should.equal(1);
done();
});
node.emit('ready');
});
});
});

300
test/transaction.unit.js Normal file
View File

@ -0,0 +1,300 @@
'use strict';
var should = require('chai').should();
var sinon = require('sinon');
var bitcoinlib = require('../');
var Transaction = bitcoinlib.Transaction;
var transactionData = require('./data/bitcoin-transactions.json');
var memdown = require('memdown');
var DB = bitcoinlib.DB;
var db = new DB({store: memdown});
var chainlib = require('chainlib');
var levelup = chainlib.deps.levelup;
describe('Bitcoin Transaction', function() {
describe('#validate', function() {
it('should give an error if verify() fails', function(done) {
var tx = new Transaction();
tx.verify = sinon.stub().returns('invalid tx');
tx.validate(db, [], function(err) {
should.exist(err);
err.message.should.equal('invalid tx');
done();
});
});
it('should give an error if one if the async series functions fails', function(done) {
var tx = new Transaction();
tx._validateInputs = sinon.stub().callsArg(2);
tx._validateOutputs = sinon.stub().callsArgWith(0, new Error('output validation error'));
tx._checkSufficientInputs = sinon.stub().callsArg(0);
tx.verify = sinon.stub().returns(true);
tx.validate(db, [], function(err) {
should.exist(err);
err.message.should.equal('output validation error');
tx._validateInputs.calledOnce.should.equal(true);
tx._validateOutputs.calledOnce.should.equal(true);
tx._checkSufficientInputs.called.should.equal(false);
done();
});
});
it('should call all the functions if there is no error', function(done) {
var tx = new Transaction();
tx._validateInputs = sinon.stub().callsArg(2);
tx._validateOutputs = sinon.stub().callsArg(0);
tx._checkSufficientInputs = sinon.stub().callsArg(0);
tx.verify = sinon.stub().returns(true);
tx.validate(db, [], function(err) {
should.not.exist(err);
tx._validateInputs.calledOnce.should.equal(true);
tx._validateOutputs.calledOnce.should.equal(true);
tx._checkSufficientInputs.calledOnce.should.equal(true);
done();
});
});
});
describe('#_validateInputs', function() {
it('should call all the functions and complete when no errors', function(done) {
var tx = new Transaction();
tx.inputs = ['input'];
sinon.stub(tx, '_populateInput', function(db, input, poolTransactions, callback) {
return callback(null, input, 'populateInput');
});
sinon.stub(tx, '_checkSpent', function(db, input, poolTransactions, callback) {
return callback();
});
sinon.stub(tx, '_checkScript', function(input, index, callback) {
return callback();
});
tx._validateInputs('db', [], function(err) {
should.not.exist(err);
tx._populateInput.calledOnce.should.equal(true);
tx._populateInput.calledWith('db', 'input');
tx._checkSpent.calledOnce.should.equal(true);
tx._populateInput.calledWith('db', 'input');
tx._checkScript.calledOnce.should.equal(true);
tx._populateInput.calledWith('input');
done();
});
});
it('should halt on an error', function(done) {
var tx = new Transaction();
tx.inputs = ['input'];
sinon.stub(tx, '_populateInput', function(db, input, poolTransactions, callback) {
return callback();
});
sinon.stub(tx, '_checkSpent', function(db, input, poolTransactions, callback) {
return callback(new Error('error'));
});
sinon.stub(tx, '_checkScript', function(input, callback) {
return callback();
});
tx._validateInputs('db', [], function(err) {
should.exist(err);
err.message.should.equal('error');
tx._populateInput.calledOnce.should.equal(true);
tx._populateInput.calledWith('input');
tx._checkSpent.calledOnce.should.equal(true);
tx._populateInput.calledWith('input');
tx._checkScript.called.should.equal(false);
done();
});
});
});
describe('#populateInputs', function() {
it('will call _populateInput with transactions', function() {
var tx = new Transaction();
tx._populateInput = sinon.stub().callsArg(3);
tx.inputs = ['input'];
var transactions = [];
var db = {};
tx.populateInputs(db, transactions, function(err) {
tx._populateInput.callCount.should.equal(1);
tx._populateInput.args[0][0].should.equal(db);
tx._populateInput.args[0][1].should.equal('input');
tx._populateInput.args[0][2].should.equal(transactions);
});
});
});
describe('#_populateInput', function() {
var input = {
prevTxId: new Buffer('d6cffbb343a6a41eeaa199478c985493843bfe6a59d674a5c188787416cbcda3', 'hex'),
outputIndex: 0
};
it('should give an error if the input does not have a valid prevTxId', function(done) {
var badInput = {
prevTxId: 'bad'
};
var tx = new Transaction();
tx._populateInput({}, badInput, [], function(err) {
should.exist(err);
err.message.should.equal('Input is expected to have prevTxId as a buffer');
done();
});
});
it('if an error happened it should pass it along', function(done) {
var tx = new Transaction();
var db = {
getTransactionFromDB: sinon.stub().callsArgWith(1, new Error('error'))
};
tx._populateInput(db, input, [], function(err) {
should.exist(err);
err.message.should.equal('error');
done();
});
});
it('should return an error if the transaction for the input does not exist', function(done) {
var tx = new Transaction();
var db = {
getTransactionFromDB: sinon.stub().callsArgWith(1, new levelup.errors.NotFoundError())
};
tx._populateInput(db, input, [], function(err) {
should.exist(err);
err.message.should.equal('Previous tx ' + input.prevTxId.toString('hex') + ' not found');
done();
});
});
it('should look through poolTransactions if database does not have transaction', function(done) {
var tx = new Transaction();
var db = {
getTransactionFromDB: sinon.stub().callsArgWith(1, new levelup.errors.NotFoundError())
};
var transactions = [
{
hash: 'd6cffbb343a6a41eeaa199478c985493843bfe6a59d674a5c188787416cbcda3',
outputs: ['output']
}
];
tx._populateInput(db, input, transactions, function(err) {
should.not.exist(err);
input.output.should.equal('output');
done();
});
});
it('should not return an error if an error did not occur', function(done) {
var prevTx = new Transaction();
prevTx.outputs = ['output'];
var tx = new Transaction();
var db = {
getTransactionFromDB: sinon.stub().callsArgWith(1, null, prevTx)
};
tx._populateInput(db, input, [], function(err) {
should.not.exist(err);
input.output.should.equal('output');
done();
});
});
});
describe('#_checkSpent', function() {
it('should return an error if input was spent', function(done) {
var tx = new Transaction();
var db = {
isSpentDB: sinon.stub().callsArgWith(1, true)
};
tx._checkSpent(db, [], 'input', function(err) {
should.exist(err);
err.message.should.equal('Input already spent');
done();
});
});
it('should not return an error if input was unspent', function(done) {
var tx = new Transaction();
var db = {
isSpentDB: sinon.stub().callsArgWith(1, false)
};
tx._checkSpent(db, [], 'input', function(err) {
should.not.exist(err);
done();
});
});
});
describe('#_checkScript', function() {
it('should not have an error with a valid script', function(done) {
var prevTx = new Transaction();
prevTx.fromString(transactionData[0].hex);
var tx = new Transaction();
tx.fromString(transactionData[1].hex);
var input = tx.inputs[0];
input.output = prevTx.outputs[0];
tx._checkScript(input, 0, function(err) {
should.not.exist(err);
done();
});
});
it('should have an error when signature is missing', function(done) {
var prevTx = new Transaction();
prevTx.fromString(transactionData[0].hex);
var tx = new Transaction();
tx.fromString(transactionData[2].hex);
var input = tx.inputs[0];
input.output = prevTx.outputs[0];
tx._checkScript(input, 0, function(err) {
should.exist(err);
done();
});
});
});
describe('#_checkSufficientInputs', function() {
var inputs = [
{
outputIndex: 0,
output: {
satoshis: 1000
}
},
{
outputIndex: 0,
output: {
satoshis: 2000
}
},
{
outputIndex: 1,
output: {
satoshis: 3000
}
},
];
var outputs = [
{
satoshis: 4000
},
{
satoshis: 3000
}
];
it('should give an error if inputs are less than outputs', function(done) {
var tx = new Transaction();
tx.inputs = inputs;
tx.outputs = outputs;
tx._checkSufficientInputs(function(err) {
should.exist(err);
err.message.should.equal('Insufficient inputs');
done();
});
});
it('should not give an error if inputs are greater than or equal to outputs', function(done) {
inputs[2].output = {
satoshis: 8000
};
var tx = new Transaction();
tx.inputs = inputs;
tx.outputs = outputs;
tx._checkSufficientInputs(function(err) {
should.not.exist(err);
done();
});
});
});
});