diff --git a/lib/chain/chain.js b/lib/chain/chain.js index 8cb21356..8cd23c73 100644 --- a/lib/chain/chain.js +++ b/lib/chain/chain.js @@ -1086,7 +1086,7 @@ Chain.prototype._replay = co(function* replay(block) { Chain.prototype.scan = co(function* scan(start, filter, iter) { var unlock = yield this.locker.lock(); try { - return yield this.db.scan(block, filter, iter); + return yield this.db.scan(start, filter, iter); } finally { unlock(); } diff --git a/lib/wallet/records.js b/lib/wallet/records.js index df5cf7f6..f3379472 100644 --- a/lib/wallet/records.js +++ b/lib/wallet/records.js @@ -24,6 +24,7 @@ function ChainState() { this.startHeight = -1; this.startHash = constants.NULL_HASH; this.height = -1; + this.marked = false; } /** @@ -36,6 +37,7 @@ ChainState.prototype.clone = function clone() { state.startHeight = this.startHeight; state.startHash = this.startHash; state.height = this.height; + state.marked = this.marked; return state; }; @@ -47,9 +49,15 @@ ChainState.prototype.clone = function clone() { ChainState.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); + this.startHeight = p.readU32(); this.startHash = p.readHash('hex'); this.height = p.readU32(); + this.marked = true; + + if (p.left() > 0) + this.marked = p.readU8() === 1; + return this; }; @@ -75,6 +83,7 @@ ChainState.prototype.toRaw = function toRaw(writer) { p.writeU32(this.startHeight); p.writeHash(this.startHash); p.writeU32(this.height); + p.writeU8(this.marked ? 1 : 0); if (!writer) p = p.render(); diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 7f46401d..f1a3bbe4 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -504,6 +504,7 @@ TXDB.prototype.resolve = co(function* resolve(tx, block) { */ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx, block) { + var flags = constants.flags.MANDATORY_VERIFY_FLAGS; var hash = tx.hash('hex'); var hasOrphans = false; var orphans = []; @@ -527,7 +528,7 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx, block) { if (coin) { if (this.options.verify && tx.height === -1) { input.coin = coin; - if (!(yield tx.verifyInputAsync(i))) + if (!(yield tx.verifyInputAsync(i, flags))) return false; } @@ -547,7 +548,7 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx, block) { if (coin) { if (this.options.verify && tx.height === -1) { input.coin = coin; - if (!(yield tx.verifyInputAsync(i))) + if (!(yield tx.verifyInputAsync(i, flags))) return false; } continue; @@ -618,6 +619,7 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx, block) { */ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, block, resolved) { + var flags = constants.flags.MANDATORY_VERIFY_FLAGS; var hash = tx.hash('hex'); var i, j, input, output, key; var orphans, orphan, coin, valid; @@ -661,7 +663,7 @@ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, block, resolved) // We can finally verify this input. if (this.options.verify && orphan.tx.height === -1) { input.coin = coin; - valid = yield orphan.tx.verifyInputAsync(orphan.index); + valid = yield orphan.tx.verifyInputAsync(orphan.index, flags); } // If it's valid and fully resolved, diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 7f6902a1..06c24781 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -1541,7 +1541,7 @@ WalletDB.prototype.init = co(function* init() { 'Initializing WalletDB chain state at %s (%d).', utils.revHex(tip.hash), tip.height); - yield this.resetState(tip); + yield this.resetState(tip, false); }); /** @@ -1564,7 +1564,7 @@ WalletDB.prototype.getState = co(function* getState() { * @returns {Promise} */ -WalletDB.prototype.resetState = co(function* resetState(tip) { +WalletDB.prototype.resetState = co(function* resetState(tip, marked) { var batch = this.db.batch(); var state = this.state.clone(); var iter, item; @@ -1592,6 +1592,7 @@ WalletDB.prototype.resetState = co(function* resetState(tip) { state.startHeight = tip.height; state.startHash = tip.hash; state.height = tip.height; + state.marked = marked; batch.put(layout.h(tip.height), tip.toHash()); batch.put(layout.R, state.toRaw()); @@ -1646,6 +1647,22 @@ WalletDB.prototype.syncState = co(function* syncState(tip) { this.state = state; }); +/** + * Mark the start block once a confirmed tx is seen. + * @param {BlockMeta} tip + * @returns {Promise} + */ + +WalletDB.prototype.maybeMark = co(function* maybeMark(tip) { + if (this.state.marked) + return; + + this.logger.info('Marking WalletDB start block at %s (%d).', + utils.revHex(tip.hash), tip.height); + + yield this.resetState(tip, true); +}); + /** * Get a block->wallet map. * @param {Number} height @@ -1770,14 +1787,14 @@ WalletDB.prototype.getTip = co(function* getTip() { */ WalletDB.prototype.rollback = co(function* rollback(height) { - var tip; + var tip, marked; if (height > this.state.height) throw new Error('WDB: Cannot rollback to the future.'); if (height === this.state.height) { this.logger.debug('Rolled back to same height (%d).', height); - return; + return true; } this.logger.info( @@ -1789,7 +1806,7 @@ WalletDB.prototype.rollback = co(function* rollback(height) { if (tip) { yield this.revert(tip.height); yield this.syncState(tip); - return; + return true; } tip = new BlockMeta(); @@ -1797,6 +1814,7 @@ WalletDB.prototype.rollback = co(function* rollback(height) { if (height >= this.state.startHeight) { tip.height = this.state.startHeight; tip.hash = this.state.startHash; + marked = this.state.marked; this.logger.warning( 'Rolling back WalletDB to start block (%d).', @@ -1804,12 +1822,15 @@ WalletDB.prototype.rollback = co(function* rollback(height) { } else { tip.height = 0; tip.hash = this.network.genesis.hash; + marked = false; this.logger.warning('Rolling back WalletDB to genesis block.'); } yield this.revert(tip.height); - yield this.resetState(tip); + yield this.resetState(tip, marked); + + return false; }); /** @@ -1899,6 +1920,7 @@ WalletDB.prototype._addBlock = co(function* addBlock(entry, txs) { throw new Error('WDB: Bad connection (height mismatch).'); } + // Sync the state to the new tip. yield this.syncState(tip); if (this.options.useCheckpoints) { @@ -1962,6 +1984,7 @@ WalletDB.prototype._removeBlock = co(function* removeBlock(entry) { if (!prev) throw new Error('WDB: Bad disconnection (no previous block).'); + // Get the map of txids->wids. block = yield this.getBlockMap(tip.height); if (!block) { @@ -1974,6 +1997,7 @@ WalletDB.prototype._removeBlock = co(function* removeBlock(entry) { yield this._unconfirm(tx); } + // Sync the state to the previous tip. yield this.syncState(prev); this.logger.warning('Disconnected wallet block %s (tx=%d).', @@ -2036,6 +2060,13 @@ WalletDB.prototype._insert = co(function* insert(tx, block) { 'Incoming transaction for %d wallets in WalletDB (%s).', wids.length, tx.rhash); + // If this is our first transaction + // in a block, set the start block here. + if (block) + yield this.maybeMark(block); + + // Insert the transaction + // into every matching wallet. for (i = 0; i < wids.length; i++) { wid = wids[i]; wallet = yield this.get(wid); @@ -2101,7 +2132,13 @@ WalletDB.prototype._resetChain = co(function* resetChain(entry) { if (entry.height > this.state.height) throw new Error('WDB: Bad reset height.'); - yield this.scan(entry.height); + // Try to rollback. + if (yield this.rollback(entry.height)) + return; + + // If we rolled back to the + // start block, we need a rescan. + yield this.scan(); }); /* diff --git a/test/chain-test.js b/test/chain-test.js index c38e8476..b30a0ee3 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -85,6 +85,7 @@ describe('Chain', function() { it('should mine a block', cob(function* () { var block = yield miner.mineBlock(); assert(block); + yield chain.add(block); })); it('should mine competing chains', cob(function* () { @@ -121,15 +122,15 @@ describe('Chain', function() { yield co.timeout(100); balance = yield wallet.getBalance(); - assert.equal(balance.unconfirmed, 500 * 1e8); - assert.equal(balance.confirmed, 500 * 1e8); + assert.equal(balance.unconfirmed, 550 * 1e8); + assert.equal(balance.confirmed, 550 * 1e8); })); it('should handle a reorg', cob(function* () { var entry, block, forked; assert.equal(walletdb.state.height, chain.height); - assert.equal(chain.height, 10); + assert.equal(chain.height, 11); entry = yield chain.db.get(tip2.hash); assert(entry); @@ -158,8 +159,8 @@ describe('Chain', function() { yield co.timeout(100); balance = yield wallet.getBalance(); - assert.equal(balance.unconfirmed, 1050 * 1e8); - assert.equal(balance.confirmed, 550 * 1e8); + assert.equal(balance.unconfirmed, 1100 * 1e8); + assert.equal(balance.confirmed, 600 * 1e8); })); it('should check main chain', cob(function* () { @@ -221,8 +222,8 @@ describe('Chain', function() { yield co.timeout(100); balance = yield wallet.getBalance(); - assert.equal(balance.unconfirmed, 1200 * 1e8); - assert.equal(balance.confirmed, 700 * 1e8); + assert.equal(balance.unconfirmed, 1250 * 1e8); + assert.equal(balance.confirmed, 750 * 1e8); assert(wallet.account.receiveDepth >= 8); assert(wallet.account.changeDepth >= 7); @@ -230,7 +231,7 @@ describe('Chain', function() { assert.equal(walletdb.state.height, chain.height); txs = yield wallet.getHistory(); - assert.equal(txs.length, 44); + assert.equal(txs.length, 45); })); it('should get tips and remove chains', cob(function* () { @@ -255,7 +256,7 @@ describe('Chain', function() { return Promise.resolve(); }); - assert.equal(total, 25); + assert.equal(total, 26); })); it('should activate csv', cob(function* () { @@ -265,7 +266,7 @@ describe('Chain', function() { state = yield chain.getState(prev, 'csv'); assert(state === 0); - for (i = 0; i < 418; i++) { + for (i = 0; i < 417; i++) { block = yield miner.mineBlock(); yield chain.add(block); switch (chain.height) {