diff --git a/lib/chain/chaindb.js b/lib/chain/chaindb.js index 18fcf299..c51c418f 100644 --- a/lib/chain/chaindb.js +++ b/lib/chain/chaindb.js @@ -23,6 +23,7 @@ var Coin = require('../primitives/coin'); var TX = require('../primitives/tx'); var Address = require('../primitives/address'); var ChainEntry = require('./chainentry'); +var U32 = utils.U32; var DUMMY = new Buffer([0]); /* @@ -1135,11 +1136,8 @@ ChainDB.prototype.save = co(function* save(entry, block, view) { ChainDB.prototype._save = co(function* save(entry, block, view) { var hash = block.hash(); - var height = new Buffer(4); - height.writeUInt32LE(entry.height, 0, true); - - this.put(layout.h(hash), height); + this.put(layout.h(hash), U32(entry.height)); this.put(layout.e(hash), entry.toRaw()); this.cacheHash.set(entry.hash, entry); diff --git a/lib/utils/utils.js b/lib/utils/utils.js index dd335fb8..b9390119 100644 --- a/lib/utils/utils.js +++ b/lib/utils/utils.js @@ -1927,6 +1927,18 @@ utils.VerifyResult = function VerifyResult() { this.score = 0; }; +/** + * Serialize number as a u32le. + * @param {Number} num + * @returns {Buffer} + */ + +utils.U32 = function U32(num) { + var data = new Buffer(4); + data.writeUInt32LE(num, 0, true); + return data; +}; + /** * Create a lazy loader. * @param {Function} require diff --git a/lib/wallet/browser.js b/lib/wallet/browser.js index bbd85fae..369821b6 100644 --- a/lib/wallet/browser.js +++ b/lib/wallet/browser.js @@ -45,8 +45,11 @@ layout.walletdb = { return [+key.slice(1, 11), key.slice(11)]; }, R: 'R', - b: function b(hash) { - return 'b' + hash; + b: function b(height) { + return 'b' + pad32(height); + }, + bb: function bb(key) { + return +key.slice(1); }, e: function e(hash) { return 'e' + hash; diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 2aec72a1..b87ee3f7 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -455,23 +455,27 @@ TXDB.prototype.hasPath = function hasPath(output) { * @returns {Promise} */ -TXDB.prototype.resolve = co(function* add(tx) { - var hash, result; +TXDB.prototype.resolve = co(function* resolve(tx, block) { + var orphan, hash, result; - if (!this.options.resolution) - return [tx]; + if (!this.options.resolution) { + orphan = new ResolvedOrphan(tx, block); + return [orphan]; + } hash = tx.hash('hex'); - if (yield this.hasTX(hash)) - return [tx]; + if (yield this.hasTX(hash)) { + orphan = new ResolvedOrphan(tx, block); + return [orphan]; + } - result = yield this.verifyInputs(tx); + result = yield this.verifyInputs(tx, block); if (!result) return []; - return yield this.resolveOutputs(tx); + return yield this.resolveOutputs(tx, block); }); /** @@ -481,7 +485,7 @@ TXDB.prototype.resolve = co(function* add(tx) { * @returns {Promise} */ -TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { +TXDB.prototype.verifyInputs = co(function* verifyInputs(tx, block) { var hash = tx.hash('hex'); var hasOrphans = false; var orphans = []; @@ -573,7 +577,7 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { if (!this.count[hash]) this.count[hash] = 0; - this.orphans[key].push(new Orphan(tx, i)); + this.orphans[key].push(new Orphan(tx, i, block)); this.count[hash]++; this.totalOrphans++; @@ -595,7 +599,7 @@ TXDB.prototype.verifyInputs = co(function* verifyInputs(tx) { * @returns {Promise} */ -TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) { +TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, block, resolved) { var hash = tx.hash('hex'); var i, j, input, output, key; var orphans, orphan, coin, valid; @@ -607,7 +611,7 @@ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) { // Necessary for the first resolved tx // as well as the recursive behavior // below. - resolved.push(tx); + resolved.push(new ResolvedOrphan(tx, block)); for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; @@ -647,7 +651,7 @@ TXDB.prototype.resolveOutputs = co(function* resolveOutputs(tx, resolved) { if (valid) { if (--this.count[orphan.hash] === 0) { delete this.count[orphan.hash]; - yield this.resolveOutputs(orphan.tx, resolved); + yield this.resolveOutputs(orphan.tx, orphan.block, resolved); } break; } @@ -897,8 +901,12 @@ TXDB.prototype._add = co(function* add(tx, block) { if (existing) { // Existing tx is already confirmed. Ignore. - if (existing.height !== -1) + if (existing.height !== -1) { + if (block) { + // XXX need to return true here, could be a rescan. + } return; + } // The incoming tx won't confirm the // existing one anyway. Ignore. @@ -909,7 +917,7 @@ TXDB.prototype._add = co(function* add(tx, block) { tx.ps = existing.ps; // Confirm transaction. - return yield this.confirm(tx, block); + return yield this._confirm(tx, block); } if (tx.height === -1) { @@ -1126,65 +1134,58 @@ TXDB.prototype.addTXRecord = co(function* addTXRecord(tx) { this.walletdb.writeTX(this.wallet, hash, wids); }); -TXDB.prototype.addBlockRecord = co(function* addBlockRecord(tx, record) { - var hash = tx.hash('hex'); - var block = yield this.walletdb.getBlock(record.hash); - - if (!block) - block = record.clone(); - - if (block.hashes.indexOf(hash) !== -1) - return; - - block.hashes.push(hash); - - this.walletdb.writeBlock(this.wallet, block); -}); - TXDB.prototype.removeTXRecord = co(function* removeTXRecord(tx) { var hash = tx.hash('hex'); var wids = yield this.walletdb.getWalletsByTX(hash); var result; if (!wids) - return true; + return; result = utils.binaryRemove(wids, this.wallet.wid, cmp); if (!result) - return false; + return; - if (wids.length > 0) { - this.walletdb.writeTX(this.wallet, hash, wids); - return false; + yield this.removeBlockRecord(tx, tx.height); + + if (wids.length === 0) { + this.walletdb.unwriteTX(this.wallet, hash); + return; } - this.walletdb.removeTX(this.wallet, hash); - - return true; + this.walletdb.writeTX(this.wallet, hash, wids); }); -TXDB.prototype.removeBlockRecord = co(function* removeBlockRecord(tx, blk) { +TXDB.prototype.addBlockRecord = co(function* addBlockRecord(tx, record) { var hash = tx.hash('hex'); - var block = yield this.walletdb.getBlock(blk); - var index; + var block = yield this.walletdb.getBlock(record.height); + + if (!block) + block = record.clone(); + + if (!block.add(hash, this.wallet.wid)) + return; + + this.walletdb.writeBlock(this.wallet, block); +}); + +TXDB.prototype.removeBlockRecord = co(function* removeBlockRecord(tx, height) { + var hash = tx.hash('hex'); + var block = yield this.walletdb.getBlock(height); if (!block) return; - index = block.hashes.indexOf(hash); - - if (index === -1) + if (!block.remove(hash, this.wallet.wid)) return; - block.hashes.splice(index, 1); - - if (block.hashes.length > 0) { - this.walletdb.writeBlock(this.wallet, block); + if (block.txs.length === 0) { + this.walletdb.unwriteBlock(this.wallet, block); return; } - this.walletdb.removeBlock(this.wallet, block); + this.walletdb.writeBlock(this.wallet, block); }); @@ -1199,13 +1200,48 @@ function cmp(a, b) { * @returns {Promise} */ -TXDB.prototype.confirm = co(function* confirm(tx, block) { +TXDB.prototype.confirm = co(function* confirm(hash, block) { + var tx = yield this.getTX(hash); + var details; + + if (!tx) + return; + + tx.height = block.height; + tx.block = block.hash; + tx.index = -1; + tx.ts = utils.now(); + + this.start(); + + try { + details = yield this._confirm(tx, block); + } catch (e) { + this.drop(); + throw e; + } + + yield this.commit(); + + return details; +}); + +/** + * Attempt to confirm a transaction. + * @private + * @param {TX} tx + * @returns {Promise} + */ + +TXDB.prototype._confirm = co(function* confirm(tx, block) { var hash = tx.hash('hex'); var height = block ? block.height : this.walletdb.height; var details = new Details(this, tx, height); var i, account, output, coin, input, prevout; var path, credit, credits; + assert(tx.height !== -1); + if (!tx.isCoinbase()) { credits = yield this.getSpentCredits(tx); @@ -1427,10 +1463,7 @@ TXDB.prototype.erase = co(function* erase(tx) { this.del(layout.H(account, tx.height, hash)); } - if (yield this.removeTXRecord(tx)) { - if (tx.block) - yield this.removeBlockRecord(tx, tx.block); - } + yield this.removeTXRecord(tx); // Update the transaction counter // and commit new state due to @@ -1601,6 +1634,8 @@ TXDB.prototype.disconnect = co(function* disconnect(tx, block) { this.saveCredit(credit, path); } + yield this.removeBlockRecord(tx, block.height); + // We need to update the now-removed // block properties and reindex due // to the height change. @@ -3168,10 +3203,16 @@ DetailsMember.prototype.toJSON = function toJSON(network) { * Helpers */ -function Orphan(tx, index) { +function Orphan(tx, index, block) { this.tx = tx; this.hash = tx.hash('hex'); this.index = index; + this.block = block || null; +} + +function ResolvedOrphan(tx, block) { + this.tx = tx; + this.block = block || null; } function cmp(a, b) { diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 8b64734b..270cdb41 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -1833,13 +1833,14 @@ Wallet.prototype.add = co(function* add(tx, block) { Wallet.prototype._add = co(function* add(tx, block) { var resolved = yield this.txdb.resolve(tx, block); var result = false; - var i; + var i, orphan; if (resolved.length === 0) return true; for (i = 0; i < resolved.length; i++) { - if (yield this._insert(resolved[i], block)) + orphan = resolved[i]; + if (yield this._insert(orphan.tx, orphan.block)) result = true; } @@ -1891,6 +1892,21 @@ Wallet.prototype.unconfirm = co(function* unconfirm(hash, block) { } }); +/** + * Confirm a wallet transcation. + * @param {Hash} hash + * @returns {Promise} + */ + +Wallet.prototype.confirm = co(function* confirm(hash, block) { + var unlock = yield this.writeLock.lock(); + try { + return yield this.txdb.confirm(hash, block); + } finally { + unlock(); + } +}); + /** * Remove a wallet transaction. * @param {Hash} hash diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index 8e5bd0c0..3f9f246d 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -26,6 +26,7 @@ var Bloom = require('../utils/bloom'); var Logger = require('../node/logger'); var TX = require('../primitives/tx'); var TXDB = require('./txdb'); +var U32 = utils.U32; /* * Database Layout: @@ -101,12 +102,15 @@ var layout = { return [key.readUInt32BE(1, true), key.toString('ascii', 5)]; }, R: new Buffer([0x52]), - b: function b(hash) { - var key = new Buffer(33); + b: function b(height) { + var key = new Buffer(5); key[0] = 0x62; - key.write(hash, 1, 'hex'); + key.writeUInt32BE(height, 1, true); return key; }, + bb: function bb(key) { + return key.readUInt32BE(1, true); + }, e: function e(hash) { var key = new Buffer(33); key[0] = 0x65; @@ -287,6 +291,21 @@ WalletDB.prototype.getDepth = co(function* getDepth() { return depth + 1; }); +/** + * Get current block height. + * @private + * @returns {Promise} + */ + +WalletDB.prototype.getHeight = co(function* getHeight() { + var data = yield this.db.get(layout.R); + + if (!data) + return -1; + + return data.readUInt32LE(0, true); +}); + /** * Start batch. * @private @@ -538,14 +557,11 @@ WalletDB.prototype.save = function save(wallet) { var wid = wallet.wid; var id = wallet.id; var batch = this.batch(wallet); - var buf = new Buffer(4); this.widCache.set(id, wid); batch.put(layout.w(wid), wallet.toRaw()); - - buf.writeUInt32LE(wid, 0, true); - batch.put(layout.l(id), buf); + batch.put(layout.l(id), U32(wid)); }; /** @@ -788,12 +804,9 @@ WalletDB.prototype.saveAccount = function saveAccount(account) { var index = account.accountIndex; var name = account.name; var batch = this.batch(wallet); - var buf = new Buffer(4); - - buf.writeUInt32LE(index, 0, true); batch.put(layout.a(wid, index), account.toRaw()); - batch.put(layout.i(wid, name), buf); + batch.put(layout.i(wid, name), U32(index)); wallet.accountCache.set(index, account); }; @@ -1079,14 +1092,19 @@ WalletDB.prototype._rescan = co(function* rescan(chaindb, height) { var self = this; var hashes; - if (height == null) - height = this.height; + if (height == null) { + height = this.height - 36; + if (height < 0) + height = 0; + } + + yield this.rollback(height); hashes = yield this.getHashes(); this.logger.info('Scanning for %d addresses.', hashes.length); - yield chaindb.scan(height, hashes, function(block, txs) { + yield chaindb.scan(this.tip, hashes, function(block, txs) { return self._addBlock(block, txs); }); }); @@ -1313,7 +1331,8 @@ WalletDB.prototype.writeGenesis = co(function* writeGenesis() { */ WalletDB.prototype.getTip = co(function* getTip() { - var data = yield this.db.get(layout.R); + var height = yield this.getHeight(); + var data = yield this.db.get(layout.b(height)); if (!data) return; @@ -1322,16 +1341,32 @@ WalletDB.prototype.getTip = co(function* getTip() { }); /** - * Write the best block hash. + * Write the tip immediately. * @param {Hash} hash * @param {Number} height * @returns {Promise} */ -WalletDB.prototype.setTip = co(function* setTip(hash, height) { +WalletDB.prototype.setTip = function setTip(hash, height) { var block = new WalletBlock(hash, height); + return this.setBlock(block); +}; - yield this.db.put(layout.R, block.toTip()); +/** + * Write the connecting block immediately. + * @param {Hash} hash + * @param {Number} height + * @returns {Promise} + */ + +WalletDB.prototype.setBlock = co(function* setBlock(block) { + var batch = this.db.batch(); + + batch.del(layout.b(block.height + 1)); + batch.put(layout.b(block.height), block.toRaw()); + batch.put(layout.R, U32(block.height)); + + yield batch.write(); this.tip = block.hash; this.height = block.height; @@ -1345,7 +1380,20 @@ WalletDB.prototype.setTip = co(function* setTip(hash, height) { WalletDB.prototype.writeBlock = function writeBlock(wallet, block) { var batch = this.batch(wallet); - batch.put(layout.b(block.hash), block.toRaw()); + batch.put(layout.b(block.height), block.toRaw()); + batch.put(layout.R, U32(block.height)); +}; + +/** + * Connect a block. + * @param {WalletBlock} block + * @returns {Promise} + */ + +WalletDB.prototype.unwriteBlock = function unwriteBlock(wallet, block) { + var batch = this.batch(wallet); + batch.del(layout.b(block.height)); + batch.put(layout.R, U32(block.height - 1)); }; /** @@ -1356,19 +1404,8 @@ WalletDB.prototype.writeBlock = function writeBlock(wallet, block) { WalletDB.prototype.writeTX = function writeTX(wallet, hash, wids) { var batch = this.batch(wallet); - this.addFilter(hash); batch.put(layout.e(hash), serializeWallets(wids)); -}; - -/** - * Connect a block. - * @param {WalletBlock} block - * @returns {Promise} - */ - -WalletDB.prototype.removeBlock = function removeBlock(wallet, block) { - var batch = this.batch(wallet); - batch.del(layout.b(block.hash)); + this.addFilter(hash); }; /** @@ -1377,7 +1414,7 @@ WalletDB.prototype.removeBlock = function removeBlock(wallet, block) { * @returns {Promise} */ -WalletDB.prototype.removeTX = function removeTX(wallet, hash) { +WalletDB.prototype.unwriteTX = function unwriteTX(wallet, hash) { var batch = this.batch(wallet); batch.del(layout.e(hash)); }; @@ -1388,13 +1425,13 @@ WalletDB.prototype.removeTX = function removeTX(wallet, hash) { * @returns {Promise} */ -WalletDB.prototype.getBlock = co(function* getBlock(hash) { - var data = yield this.db.get(layout.b(hash)); +WalletDB.prototype.getBlock = co(function* getBlock(height) { + var data = yield this.db.get(layout.b(height)); if (!data) return; - return WalletBlock.fromRaw(hash, data); + return WalletBlock.fromRaw(data); }); /** @@ -1412,6 +1449,28 @@ WalletDB.prototype.getWalletsByTX = co(function* getWalletsByTX(hash) { return parseWallets(data); }); +/** + * Sync with chain height. + * @param {Number} height + * @returns {Promise} + */ + +WalletDB.prototype.rollback = co(function* rollback(height) { + var block; + + if (this.height > height) { + this.logger.info( + 'Rolling back %d blocks to height %d.', + this.height - height, height); + } + + while (this.height > height) { + block = yield this.getBlock(this.height); + assert(block); + yield this._removeBlock(block); + } +}); + /** * Add a block's transactions and write the new best hash. * @param {ChainEntry} entry @@ -1421,6 +1480,7 @@ WalletDB.prototype.getWalletsByTX = co(function* getWalletsByTX(hash) { WalletDB.prototype.addBlock = co(function* addBlock(entry, txs) { var unlock = yield this.txLock.lock(); try { + yield this.rollback(entry.height - 1); return yield this._addBlock(entry, txs); } finally { unlock(); @@ -1440,7 +1500,8 @@ WalletDB.prototype._addBlock = co(function* addBlock(entry, txs) { if (this.options.useCheckpoints) { if (entry.height <= this.network.checkpoints.lastHeight) { - yield this.setTip(entry.hash, entry.height); + block = WalletBlock.fromEntry(entry); + yield this.setBlock(block); return; } } @@ -1456,12 +1517,18 @@ WalletDB.prototype._addBlock = co(function* addBlock(entry, txs) { total++; } - if (total > 0) { - this.logger.info('Connecting block %s (%d txs).', - utils.revHex(block.hash), total); + if (total === 0) { + yield this.setBlock(block); + return total; } - yield this.setTip(entry.hash, entry.height); + this.height = block.height; + this.tip = block.hash; + + this.logger.info('Connected block %s (tx=%d).', + utils.revHex(block.hash), total); + + return total; }); /** @@ -1474,6 +1541,7 @@ WalletDB.prototype._addBlock = co(function* addBlock(entry, txs) { WalletDB.prototype.removeBlock = co(function* removeBlock(entry) { var unlock = yield this.txLock.lock(); try { + yield this.rollback(entry.height); return yield this._removeBlock(entry); } finally { unlock(); @@ -1488,24 +1556,34 @@ WalletDB.prototype.removeBlock = co(function* removeBlock(entry) { */ WalletDB.prototype._removeBlock = co(function* removeBlock(entry) { - var block = WalletBlock.fromEntry(entry); - var data = yield this.getBlock(block.hash); - var i, hash; + var block = yield this.getBlock(entry.height); + var i, tx, prev; - if (data) - block.hashes = data.hashes; + if (!block) + return; - if (block.hashes.length > 0) { - this.logger.warning('Disconnecting block %s (%d txs).', - utils.revHex(block.hash), block.hashes.length); + assert(block.height > 0); + + prev = yield this.getBlock(entry.height - 1); + assert(prev); + + if (block.txs.length === 0) { + yield this.setBlock(prev); + return block.txs.length; } - for (i = block.hashes.length - 1; i >= 0; i--) { - hash = block.hashes[i]; - yield this._unconfirmTX(hash, block); + for (i = block.txs.length - 1; i >= 0; i--) { + tx = block.txs[i]; + yield this._unconfirmTX(tx, block); } - yield this.setTip(block.prevBlock, block.height - 1); + this.height = prev.height; + this.tip = prev.hash; + + this.logger.warning('Disconnected block %s (tx=%d).', + utils.revHex(block.hash), block.txs.length); + + return block.txs.length; }); /** @@ -1568,17 +1646,21 @@ WalletDB.prototype._insertTX = co(function* insertTX(tx, block) { }); /** - * Unconfirm a transaction from all relevant wallets. - * @param {Hash} hash + * Confirm a transaction from all + * relevant wallets without a lock. + * @private + * @param {TXHash} hash * @returns {Promise} */ -WalletDB.prototype.unconfirmTX = co(function* unconfirmTX(hash, block) { - var unlock = yield this.txLock.lock(); - try { - return yield this._unconfirmTX(hash, block); - } finally { - unlock(); +WalletDB.prototype._confirmTX = co(function* confirmTX(tx, block) { + var i, wid, wallet; + + for (i = 0; i < tx.wids.length; i++) { + wid = tx.wids[i]; + wallet = yield this.get(wid); + assert(wallet); + yield wallet.confirm(tx.hash, block); } }); @@ -1586,22 +1668,18 @@ WalletDB.prototype.unconfirmTX = co(function* unconfirmTX(hash, block) { * Unconfirm a transaction from all * relevant wallets without a lock. * @private - * @param {Hash} hash + * @param {TXHash} hash * @returns {Promise} */ -WalletDB.prototype._unconfirmTX = co(function* unconfirmTX(hash, block) { - var wids = yield this.getWalletsByTX(hash); +WalletDB.prototype._unconfirmTX = co(function* unconfirmTX(tx, block) { var i, wid, wallet; - if (!wids) - return; - - for (i = 0; i < wids.length; i++) { - wid = wids[i]; + for (i = 0; i < tx.wids.length; i++) { + wid = tx.wids[i]; wallet = yield this.get(wid); assert(wallet); - yield wallet.unconfirm(hash, block); + yield wallet.unconfirm(tx.hash, block); } }); @@ -1652,8 +1730,8 @@ function WalletBlock(hash, height) { this.hash = hash || constants.NULL_HASH; this.height = height != null ? height : -1; - this.prevBlock = constants.NULL_HASH; - this.hashes = []; + this.txs = []; + this.index = {}; } /** @@ -1662,10 +1740,7 @@ function WalletBlock(hash, height) { */ WalletBlock.prototype.clone = function clone() { - var block = new WalletBlock(this.hash, this.height); - block.prevBlock = this.prevBlock; - block.hashes = this.hashes.slice(); - return block; + return new WalletBlock(this.hash, this.height); }; /** @@ -1677,7 +1752,6 @@ WalletBlock.prototype.clone = function clone() { WalletBlock.prototype.fromEntry = function fromEntry(entry) { this.hash = entry.hash; this.height = entry.height; - this.prevBlock = entry.prevBlock; return this; }; @@ -1690,8 +1764,6 @@ WalletBlock.prototype.fromEntry = function fromEntry(entry) { WalletBlock.prototype.fromJSON = function fromJSON(json) { this.hash = utils.revHex(json.hash); this.height = json.height; - if (json.prevBlock) - this.prevBlock = utils.revHex(json.prevBlock); return this; }; @@ -1702,12 +1774,23 @@ WalletBlock.prototype.fromJSON = function fromJSON(json) { * @param {Buffer} data */ -WalletBlock.prototype.fromRaw = function fromRaw(hash, data) { +WalletBlock.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); - this.hash = hash; + var i, hash, tx, count; + + this.hash = p.readHash('hex'); this.height = p.readU32(); - while (p.left()) - this.hashes.push(p.readHash('hex')); + + while (p.left()) { + hash = p.readHash('hex'); + tx = new TXHash(hash); + count = p.readVarint(); + for (i = 0; i < count; i++) + tx.wids.push(p.readU32()); + this.txs.push(tx); + this.index[tx.hash] = tx; + } + return this; }; @@ -1751,8 +1834,8 @@ WalletBlock.fromJSON = function fromJSON(json) { * @returns {WalletBlock} */ -WalletBlock.fromRaw = function fromRaw(hash, data) { - return new WalletBlock().fromRaw(hash, data); +WalletBlock.fromRaw = function fromRaw(data) { + return new WalletBlock().fromRaw(data); }; /** @@ -1790,12 +1873,18 @@ WalletBlock.prototype.toTip = function toTip(writer) { WalletBlock.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); - var i; + var i, j, tx; + p.writeHash(this.hash); p.writeU32(this.height); - for (i = 0; i < this.hashes.length; i++) - p.writeHash(this.hashes[i]); + for (i = 0; i < this.txs.length; i++) { + tx = this.txs[i]; + p.writeHash(tx.hash); + p.writeVarint(tx.wids.length); + for (j = 0; j < tx.wids.length; j++) + p.writeU32(tx.wids[j]); + } if (!writer) p = p.render(); @@ -1803,6 +1892,61 @@ WalletBlock.prototype.toRaw = function toRaw(writer) { return p; }; +/** + * Add a hash and wid pair to the block. + * @param {Hash} hash + * @param {WalletID} wid + * @returns {Boolean} + */ + +WalletBlock.prototype.add = function add(hash, wid) { + var tx = this.index[hash]; + var index; + + if (!tx) { + tx = new TXHash(hash); + tx.wids.push(wid); + this.txs.push(tx); + this.index[tx.hash] = tx; + return true; + } + + index = utils.binaryInsert(tx.wids, wid, cmp, true); + + if (index === -1) + return false; + + return true; +}; + +/** + * Remove a hash and wid pair from the block. + * @param {Hash} hash + * @param {WalletID} wid + * @returns {Boolean} + */ + +WalletBlock.prototype.remove = function remove(hash, wid) { + var tx = this.index[hash]; + var result; + + if (!tx) + return false; + + result = utils.binaryRemove(tx.wids, wid, cmp); + + if (!result) + return false; + + if (tx.wids.length === 0) { + result = utils.binaryRemove(this.txs, tx, cmpid); + assert(result); + delete this.index[tx.hash]; + } + + return true; +}; + /** * Convert the block to a more json-friendly object. * @returns {Object} @@ -1819,6 +1963,14 @@ WalletBlock.prototype.toJSON = function toJSON() { * Helpers */ +function TXHash(hash, wids) { + this.hash = hash || constants.NULL_HASH; + this.wids = wids || []; + this.id = TXHash.id++; +} + +TXHash.id = 0; + function parseWallets(data) { var p = new BufferReader(data); var wids = []; @@ -1845,6 +1997,10 @@ function cmp(a, b) { return a - b; } +function cmpid(a, b) { + return a.id - b.id; +} + /* * Expose */