validate sidechain's inputs after a reorg instead of before.

This commit is contained in:
Christopher Jeffrey 2016-04-15 21:42:04 -07:00
parent 8efef35828
commit 0d75c8a621
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
6 changed files with 399 additions and 143 deletions

View File

@ -753,9 +753,6 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac
if (err)
return callback(err);
if (block.getClaimed().cmp(block.getReward()) > 0)
return callback(new VerifyError(block, 'invalid', 'bad-cb-amount', 100));
// Check all transactions
for (i = 0; i < block.txs.length; i++) {
tx = block.txs[i];
@ -780,13 +777,6 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac
if (tx.isCoinbase())
continue;
if (!tx.checkInputs(height, ret)) {
return callback(new VerifyError(block,
'invalid',
ret.reason,
ret.score));
}
for (j = 0; j < tx.inputs.length; j++) {
input = tx.inputs[j];
@ -814,8 +804,18 @@ Chain.prototype._checkInputs = function _checkInputs(block, prev, flags, callbac
100));
}
}
if (!tx.checkInputs(height, ret)) {
return callback(new VerifyError(block,
'invalid',
ret.reason,
ret.score));
}
}
if (block.getClaimed().cmp(block.getReward()) > 0)
return callback(new VerifyError(block, 'invalid', 'bad-cb-amount', 100));
if (self.options.verifySync === true)
return callback();
@ -915,8 +915,9 @@ Chain.prototype._findFork = function _findFork(fork, longer, callback) {
Chain.prototype._reorganize = function _reorganize(entry, block, callback) {
var self = this;
var tip = this.tip;
return this._findFork(this.tip, entry, function(err, fork) {
return this._findFork(tip, entry, function(err, fork) {
if (err)
return callback(err);
@ -926,6 +927,8 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) {
function disconnect(callback) {
var entries = [];
entries.push(tip);
(function collect(entry) {
entry.getPrevious(function(err, entry) {
if (err)
@ -933,20 +936,20 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) {
assert(entry);
entries.push(entry);
if (entry.hash === fork.hash)
return finish();
entries.push(entry);
collect(entry);
});
})(self.tip);
})(tip);
function finish() {
assert(entries.length > 0);
utils.forEachSerial(entries, function(entry, next) {
self.db.disconnect(entry, next);
self.disconnect(entry, next);
}, callback);
}
}
@ -962,11 +965,11 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) {
assert(entry);
entries.push(entry);
if (entry.hash === fork.hash)
return finish();
entries.push(entry);
collect(entry);
});
})(entry);
@ -976,7 +979,7 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) {
assert(entries.length > 0);
utils.forEachSerial(entries, function(entry, next) {
self.db.connect(entry, next);
self.connect(entry, next);
}, callback);
}
}
@ -991,7 +994,7 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) {
self.emit('fork', block, {
height: fork.height,
expected: self.tip.hash,
expected: tip.hash,
received: entry.hash,
checkpoint: false
});
@ -1002,6 +1005,85 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) {
});
};
/**
* Disconnect an entry from the chain (updates the tip).
* @param {ChainBlock} entry
* @param {Function} callback
*/
Chain.prototype.disconnect = function disconnect(entry, callback) {
var self = this;
this.db.disconnect(entry, function(err) {
if (err)
return callback(err);
entry.getPrevious(function(err, entry) {
if (err)
return callback(err);
assert(entry);
self.tip = entry;
self.height = entry.height;
if (self.bestHeight === -1)
network.height = entry.height;
self.emit('tip', entry);
return callback();
});
});
};
/**
* Connect an entry to the chain (updates the tip).
* This will do contextual-verification on the block
* (necessary because we cannot validate the inputs
* in side chains when they come in).
* @param {ChainBlock} entry
* @param {Function} callback
*/
Chain.prototype.connect = function connect(entry, callback) {
var self = this;
this.db.getBlock(entry.hash, function(err, block) {
if (err)
return callback(err);
assert(block);
entry.getPrevious(function(err, prev) {
if (err)
return callback(err);
assert(prev);
self._verifyContext(block, prev, function(err) {
if (err)
return callback(err);
self.db.connect(entry, block, function(err) {
if (err)
return callback(err);
self.tip = entry;
self.height = entry.height;
if (self.bestHeight === -1)
network.height = entry.height;
self.emit('tip', entry);
return callback();
});
});
});
});
};
/**
* Set the best chain. This is called on every valid block
* that comes in. It may add and connect the block (main chain),
@ -1009,32 +1091,52 @@ Chain.prototype._reorganize = function _reorganize(entry, block, callback) {
* reorganize the chain (a higher fork).
* @private
* @param {ChainBlock} entry
* @param {ChainBlock} prev
* @param {Block|MerkleBlock} block
* @param {Function} callback - Returns [{@link VerifyError}].
*/
Chain.prototype._setBestChain = function _setBestChain(entry, block, callback) {
Chain.prototype._setBestChain = function _setBestChain(entry, prev, block, callback) {
var self = this;
function done(err) {
if (err)
return callback(err);
// Save block and connect inputs.
self.db.save(entry, block, true, function(err) {
if (err)
self._verifyContext(block, prev, function(err) {
if (err) {
// Couldn't verify block.
// Revert the height.
block.setHeight(-1);
if (err.type === 'VerifyError') {
self.invalid[entry.hash] = true;
self.emit('invalid', block, {
height: entry.height,
hash: entry.hash,
seen: false,
chain: false
});
}
return callback(err);
}
self.tip = entry;
self.height = entry.height;
// Save block and connect inputs.
self.db.save(entry, block, true, function(err) {
if (err)
return callback(err);
if (self.bestHeight === -1)
network.height = entry.height;
self.tip = entry;
self.height = entry.height;
self.emit('tip', entry);
if (self.bestHeight === -1)
network.height = entry.height;
// Return true (added to the main chain)
return callback(null, true);
self.emit('tip', entry);
return callback();
});
});
}
@ -1054,21 +1156,10 @@ Chain.prototype._setBestChain = function _setBestChain(entry, block, callback) {
return done();
}
// The block is on a side chain if the
// chainwork is less than or equal to
// our tip's. Add the block but do _not_
// connect the inputs.
if (entry.chainwork.cmp(this.tip.chainwork) <= 0) {
return this.db.save(entry, block, false, function(err) {
if (err)
return callback(err);
// Return false (added to side chain)
return callback(null, false);
});
}
// Everything is in order.
// Do "contextual" verification on our block
// now that we're certain its previous
// block is in the chain.
if (entry.prevBlock === this.tip.hash)
return done();
@ -1169,7 +1260,22 @@ Chain.prototype.add = function add(block, callback, force) {
(function next(block, initial) {
var hash = block.hash('hex');
var prevHash = block.prevBlock;
var height, checkpoint, orphan;
var height, checkpoint, orphan, entry;
function handleOrphans() {
// No orphan chain.
if (!self.orphan.map[hash])
return done();
// An orphan chain was found, start resolving.
block = self.orphan.map[hash];
delete self.orphan.bmap[block.hash('hex')];
delete self.orphan.map[hash];
self.orphan.count--;
self.orphan.size -= block.getSize();
next(block);
}
// Do not revalidate known invalid blocks.
if (self.invalid[hash] || self.invalid[prevHash]) {
@ -1344,76 +1450,55 @@ Chain.prototype.add = function add(block, callback, force) {
// need access to height on txs.
block.setHeight(height);
// Do "contextual" verification on our block
// now that we're certain its previous
// block is in the chain.
self._verifyContext(block, prev, function(err) {
var entry;
// Create a new chain entry.
entry = new bcoin.chainblock(self, {
hash: hash,
version: block.version,
prevBlock: block.prevBlock,
merkleRoot: block.merkleRoot,
ts: block.ts,
bits: block.bits,
nonce: block.nonce,
height: height
}, prev);
if (err) {
// Couldn't verify block.
// Revert the height.
block.setHeight(-1);
if (err.type === 'VerifyError') {
self.invalid[hash] = true;
self.emit('invalid', block, {
height: height,
hash: hash,
seen: false,
chain: false
});
}
return done(err);
}
// Create a new chain entry.
entry = new bcoin.chainblock(self, {
hash: hash,
version: block.version,
prevBlock: block.prevBlock,
merkleRoot: block.merkleRoot,
ts: block.ts,
bits: block.bits,
nonce: block.nonce,
height: height
}, prev);
// Attempt to add block to the chain index.
self._setBestChain(entry, block, function(err, mainChain) {
// The block is on a side chain if the
// chainwork is less than or equal to
// our tip's. Add the block but do _not_
// connect the inputs.
if (entry.chainwork.cmp(self.tip.chainwork) <= 0) {
return self.db.save(entry, block, false, function(err) {
if (err)
return done(err);
// Keep track of the number of blocks we
// added and the number of orphans resolved.
total++;
return callback(err);
// Emit our block (and potentially resolved
// orphan) only if it is on the main chain.
if (mainChain) {
self.emit('block', block, entry);
if (!initial)
self.emit('resolved', block, entry);
} else {
self.emit('competitor', block, entry);
if (!initial)
self.emit('competitor resolved', block, entry);
}
self.emit('competitor', block, entry);
// No orphan chain.
if (!self.orphan.map[hash])
return done();
if (!initial)
self.emit('competitor resolved', block, entry);
// An orphan chain was found, start resolving.
block = self.orphan.map[hash];
delete self.orphan.bmap[block.hash('hex')];
delete self.orphan.map[hash];
self.orphan.count--;
self.orphan.size -= block.getSize();
next(block);
handleOrphans();
});
}
// Attempt to add block to the chain index.
self._setBestChain(entry, prev, block, function(err) {
if (err)
return done(err);
// Keep track of the number of blocks we
// added and the number of orphans resolved.
total++;
// Emit our block (and potentially resolved
// orphan) only if it is on the main chain.
self.emit('block', block, entry);
if (!initial)
self.emit('resolved', block, entry);
handleOrphans();
});
});
});

View File

@ -505,37 +505,28 @@ ChainDB.prototype.getTip = function getTip(callback) {
* @param {Function} callback - Returns [Error, {@link ChainBlock}].
*/
ChainDB.prototype.connect = function connect(block, callback) {
ChainDB.prototype.connect = function connect(entry, block, callback) {
var self = this;
var batch, hash;
var batch = this.db.batch();
var hash = new Buffer(entry.hash, 'hex');
this._ensureEntry(block, function(err, entry) {
batch.put('c/n/' + entry.prevBlock, hash);
batch.put('c/h/' + pad32(entry.height), hash);
batch.put('c/t', hash);
this.cacheHash.set(entry.hash, entry);
this.cacheHeight.set(entry.height, entry);
this.emit('add entry', entry);
this.connectBlock(block, batch, function(err) {
if (err)
return callback(err);
if (!entry)
return callback();
batch = self.db.batch();
hash = new Buffer(entry.hash, 'hex');
batch.put('c/n/' + entry.prevBlock, hash);
batch.put('c/h/' + pad32(entry.height), hash);
batch.put('c/t', hash);
self.cacheHeight.set(entry.height, entry);
self.emit('add entry', entry);
self.connectBlock(entry.hash, batch, function(err) {
batch.write(function(err) {
if (err)
return callback(err);
batch.write(function(err) {
if (err)
return callback(err);
return callback(null, entry);
});
return callback(null, entry);
});
});
};
@ -555,7 +546,7 @@ ChainDB.prototype.disconnect = function disconnect(block, callback) {
return callback(err);
if (!entry)
return callback();
return callback(new Error('Entry not found.'));
batch = self.db.batch();
@ -869,14 +860,14 @@ ChainDB.prototype.disconnectBlock = function disconnectBlock(hash, batch, callba
return callback(err);
if (!block)
return callback();
return callback(new Error('Block not found.'));
if (self.options.paranoid) {
if (typeof hash === 'string')
assert(block.hash('hex') === hash, 'Database is corrupt.');
}
block.txs.forEach(function(tx) {
block.txs.slice().reverse().forEach(function(tx) {
var hash = tx.hash('hex');
var uniq = {};
@ -1448,7 +1439,7 @@ ChainDB.prototype.getBlock = function getBlock(hash, callback) {
key = 'b/b/' + hash;
self.db.get(key, function(err, data) {
if (err && errr.type !== 'NotFoundError')
if (err && err.type !== 'NotFoundError')
return callback(err);
if (!data)

View File

@ -247,7 +247,7 @@ Miner.prototype.createBlock = function createBlock(callback) {
target: target,
address: self.address,
coinbaseFlags: self.coinbaseFlags,
segwit: self.chain.segwitActive,
witness: self.chain.segwitActive,
dsha256: self.dsha256
});
@ -310,7 +310,7 @@ Miner.prototype.mineBlock = function mineBlock(callback) {
* @param {Number} options.target - Compact form.
* @param {Function} options.dsha256
* @param {Base58Address} options.address - Payout address.
* @param {Boolean} options.segwit - Allow witness
* @param {Boolean} options.witness - Allow witness
* transactions, mine a witness block.
* @property {Block} block
* @property {TX} coinbase
@ -378,7 +378,7 @@ function MinerBlock(options) {
this.block.txs.push(this.coinbase);
if (options.segwit) {
if (options.witness) {
// Set up the witness nonce and
// commitment output for segwit.
this.witness = true;
@ -444,13 +444,13 @@ MinerBlock.prototype.addTX = function addTX(tx) {
var size = this.block.getVirtualSize(true) + tx.getVirtualSize();
// Deliver me from the block size debate, please
if (size > constants.blocks.maxSize)
if (size > constants.block.maxSize)
return false;
if (this.block.hasTX(tx))
return false;
if (!this.block.witness && tx.hasWitness())
if (!this.witness && tx.hasWitness())
return false;
// Add the tx to our block

View File

@ -159,7 +159,7 @@ bcoin.miner.minerblock.prototype.mineAsync = function mineAsync(callback) {
target: this.block.bits,
address: this.options.address,
coinbaseFlags: this.options.coinbaseFlags,
segwit: this.options.segwit
witness: this.options.witness
};
return workers.call('mine', [attempt], callback);
};
@ -220,7 +220,7 @@ workers.mine = function mine(attempt) {
target: attempt.target,
address: attempt.address,
coinbaseFlags: attempt.coinbaseFlags,
segwit: attempt.segwit,
witness: attempt.witness,
dsha256: utils.dsha256
});
attempt.on('status', function(stat) {

180
test/chain-test.js Normal file
View File

@ -0,0 +1,180 @@
var bn = require('bn.js');
delete process.env.BCOIN_NETWORK;
var bcoin = require('../')({ network: 'regtest', db: 'memory' });
process.env.BCOIN_NETWORK = 'main';
var constants = bcoin.protocol.constants;
var utils = bcoin.utils;
var assert = utils.assert;
var opcodes = constants.opcodes;
constants.tx.coinbaseMaturity = 0;
describe('Chain', function() {
var chain, wallet, miner;
var competingTip, oldTip, ch1, ch2, cb1, cb2;
chain = new bcoin.chain();
wallet = new bcoin.wallet();
miner = new bcoin.miner({
chain: chain,
address: wallet.getAddress()
});
chain.on('error', function() {});
miner.on('error', function() {});
function mineBlock(entry, tx, callback) {
var realTip;
if (entry) {
realTip = chain.tip;
chain.tip = entry;
}
miner.createBlock(function(err, attempt) {
if (realTip)
chain.tip = realTip;
assert.noError(err);
if (tx) {
var redeemer = bcoin.mtx();
redeemer.addOutput({
address: wallet.getAddress(),
value: utils.satoshi('25.0')
});
redeemer.addInput(tx, 0);
wallet.sign(redeemer);
attempt.addTX(redeemer);
}
callback(null, attempt.mineSync());
});
}
function deleteCoins(tx) {
if (Array.isArray(tx)) {
tx.forEach(deleteCoins);
return;
}
tx.inputs.forEach(function(input) {
delete input.coin;
});
}
it('should open chain and miner', function(cb) {
miner.open(cb);
});
it('should mine a block', function(cb) {
miner.mineBlock(function(err, block) {
assert.noError(err);
assert(block);
cb();
});
});
it('should mine competing chains', function(cb) {
utils.forRangeSerial(0, 10, function(i, next) {
mineBlock(ch1, cb1, function(err, chain1) {
assert.noError(err);
cb1 = chain1.txs[0];
mineBlock(ch2, cb2, function(err, chain2) {
assert.noError(err);
cb2 = chain2.txs[0];
deleteCoins(chain1.txs);
chain.add(chain1, function(err) {
assert.noError(err);
deleteCoins(chain2.txs);
chain.add(chain2, function(err) {
assert.noError(err);
assert(chain.tip.hash === chain1.hash('hex'));
competingTip = chain2.hash('hex');
chain.db.get(chain1.hash('hex'), function(err, entry1) {
assert.noError(err);
chain.db.get(chain2.hash('hex'), function(err, entry2) {
assert.noError(err);
assert(entry1);
assert(entry2);
ch1 = entry1;
ch2 = entry2;
chain.db.isMainChain(chain2.hash('hex'), function(err, result) {
assert.noError(err);
assert(!result);
next();
});
});
});
});
});
});
});
}, cb);
});
it('should handle a reorg', function(cb) {
oldTip = chain.tip;
chain.db.get(competingTip, function(err, entry) {
assert.noError(err);
assert(entry);
assert(chain.height === entry.height);
chain.tip = entry;
miner.mineBlock(function(err, reorg) {
assert.noError(err);
assert(reorg);
chain.tip = oldTip;
var forked = false;
chain.once('fork', function() {
forked = true;
});
deleteCoins(reorg.txs);
chain.add(reorg, function(err) {
assert.noError(err);
assert(forked);
assert(chain.tip.hash === reorg.hash('hex'));
assert(chain.tip.chainwork.cmp(oldTip.chainwork) > 0);
cb();
});
});
});
});
it('should check main chain', function(cb) {
chain.db.isMainChain(oldTip, function(err, result) {
assert.noError(err);
assert(!result);
cb();
});
});
it('should mine a block after a reorg', function(cb) {
mineBlock(null, cb2, function(err, block) {
assert.noError(err);
deleteCoins(block.txs);
chain.add(block, function(err) {
assert.noError(err);
chain.db.get(block.hash('hex'), function(err, entry) {
assert.noError(err);
assert(entry);
assert(chain.tip.hash === entry.hash);
chain.db.isMainChain(entry.hash, function(err, result) {
assert.noError(err);
assert(result);
cb();
});
});
});
});
});
it('should fail to mine a block with coins on a side chain', function(cb) {
mineBlock(null, cb1, function(err, block) {
assert.noError(err);
deleteCoins(block.txs);
chain.add(block, function(err) {
assert(err);
cb();
});
});
});
it('should cleanup', function(cb) {
constants.tx.coinbaseMaturity = 100;
cb();
});
});

View File

@ -5,7 +5,7 @@ var utils = bcoin.utils;
var assert = utils.assert;
var opcodes = constants.opcodes;
describe('Wallet', function() {
describe('Node', function() {
var node = new bcoin.fullnode();
node.on('error', function() {});