From b835fff86ce93a7163be693f20632d04950de454 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Thu, 4 Feb 2016 18:58:56 -0800 Subject: [PATCH] hd keys. address pruning. txpool balance. --- lib/bcoin/address.js | 8 +++ lib/bcoin/hd.js | 52 ++++++++++++-- lib/bcoin/tx-pool.js | 159 +++++++++++++++++++++++++++++++++++++++---- lib/bcoin/wallet.js | 90 +++++++++++++----------- test/hd-test.js | 5 ++ test/wallet-test.js | 20 ++++-- 6 files changed, 271 insertions(+), 63 deletions(-) diff --git a/lib/bcoin/address.js b/lib/bcoin/address.js index 87563476f..c600c78dd 100644 --- a/lib/bcoin/address.js +++ b/lib/bcoin/address.js @@ -83,6 +83,14 @@ function Address(options) { inherits(Address, EventEmitter); +Address.prototype.__defineGetter__('balance', function() { + return this.getBalance(); +}); + +Address.prototype.getBalance = function getBalance() { + return this._wallet.tx.getAddressBalance(this.getAddress()); +}; + Address.prototype.setRedeem = function setRedeem(redeem) { var old = this.getScriptAddress(); diff --git a/lib/bcoin/hd.js b/lib/bcoin/hd.js index 0deeac33b..944bebc1c 100644 --- a/lib/bcoin/hd.js +++ b/lib/bcoin/hd.js @@ -85,7 +85,7 @@ function HDSeed(options) { HDSeed.prototype.createSeed = function createSeed(passphrase) { this.passphrase = passphrase || ''; - return pbkdf2(this.mnemonic, 'mnemonic' + passphrase, 2048, 64); + return pbkdf2(this.mnemonic, 'mnemonic' + this.passphrase, 2048, 64); }; HDSeed._mnemonic = function _mnemonic(entropy) { @@ -634,11 +634,16 @@ HDPrivateKey.prototype._build = function _build(data) { }; HDPrivateKey.prototype.derive = function derive(index, hardened) { - var data, hash, leftPart, chainCode, privateKey; + var cached, data, hash, leftPart, chainCode, privateKey; if (typeof index === 'string') return this.deriveString(index); + cached = cache.get(this.xprivkey, index); + + if (cached) + return cached; + hardened = index >= constants.hd.hardened ? true : hardened; if (index < constants.hd.hardened && hardened) index += constants.hd.hardened; @@ -656,7 +661,7 @@ HDPrivateKey.prototype.derive = function derive(index, hardened) { .mod(ec.curve.n) .toArray('be', 32); - return new HDPrivateKey({ + var child = new HDPrivateKey({ version: this.version, depth: new bn(this.depth).toNumber() + 1, parentFingerPrint: this.fingerPrint, @@ -665,6 +670,10 @@ HDPrivateKey.prototype.derive = function derive(index, hardened) { privateKey: privateKey, checksum: null }); + + cache.set(this.xprivkey, index, child); + + return child; }; // https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki @@ -928,11 +937,16 @@ HDPublicKey.prototype._build = function _build(data) { }; HDPublicKey.prototype.derive = function derive(index, hardened) { - var data, hash, leftPart, chainCode, pair, point, publicKey; + var cached, data, hash, leftPart, chainCode, pair, point, publicKey; if (typeof index === 'string') return this.deriveString(index); + cached = cache.get(this.xpubkey, index); + + if (cached) + return cached; + if (index >= constants.hd.hardened || hardened) throw new Error('invalid index'); @@ -948,7 +962,7 @@ HDPublicKey.prototype.derive = function derive(index, hardened) { point = ec.curve.g.mul(leftPart).add(pair.pub); publicKey = bcoin.ecdsa.keyPair({ pub: point }).getPublic(true, 'array'); - return new HDPublicKey({ + var child = new HDPublicKey({ version: this.version, depth: new bn(this.depth).toNumber() + 1, parentFingerPrint: this.fingerPrint, @@ -957,6 +971,10 @@ HDPublicKey.prototype.derive = function derive(index, hardened) { publicKey: publicKey, checksum: null }); + + cache.set(this.xpubkey, index, child); + + return child; }; HDPublicKey.isValidPath = function isValidPath(arg) { @@ -1123,6 +1141,30 @@ function pbkdf2(key, salt, iterations, dkLen) { return DK; } +var cache = { + data: {}, + count: 0 +}; + +cache.set = function(key, index, value) { + key = key + '/' + index; + + if (this.count > 200) { + this.data = {}; + this.count = 0; + } + + if (this.data[key] === undefined) + this.count++; + + this.data[key] = value; +}; + +cache.get = function(key, index) { + key = key + '/' + index; + return this.data[key]; +}; + /** * Expose */ diff --git a/lib/bcoin/tx-pool.js b/lib/bcoin/tx-pool.js index 8f2e85485..c98c44e84 100644 --- a/lib/bcoin/tx-pool.js +++ b/lib/bcoin/tx-pool.js @@ -16,6 +16,8 @@ var EventEmitter = require('events').EventEmitter; */ function TXPool(wallet) { + var self = this; + if (!(this instanceof TXPool)) return new TXPool(wallet); @@ -29,6 +31,20 @@ function TXPool(wallet) { this._orphans = {}; this._lastTs = 0; this._loaded = false; + this._addresses = {}; + this._sent = new bn(0); + this._received = new bn(0); + this._balance = new bn(0); + + this._wallet.on('remove address', function(address) { + address = self._addresses[address.getAddress()]; + if (address) { + self._balance.isub(address.balance); + self._sent.isub(address.sent); + self._received.isub(address.received); + delete self._addresses[address]; + } + }); // Load TXs from storage this._init(); @@ -120,6 +136,8 @@ TXPool.prototype.add = function add(tx, noWrite, strict) { if (!tx.verify(index)) return; + this._addInput(tx, index); + delete this._unspent[key]; updated = true; continue; @@ -167,6 +185,7 @@ TXPool.prototype.add = function add(tx, noWrite, strict) { this._removeTX(orphan.tx, noWrite); return false; } + this._addInput(orphan.tx, index); return true; } @@ -178,6 +197,8 @@ TXPool.prototype.add = function add(tx, noWrite, strict) { if (!this._wallet.ownOutput(tx, i)) continue; + this._addOutput(tx, i); + key = hash + '/' + i; orphans = this._orphans[key]; @@ -220,9 +241,15 @@ TXPool.prototype._storeTX = function _storeTX(hash, tx, noWrite) { TXPool.prototype._removeTX = function _removeTX(tx, noWrite) { var self = this; + var key; - for (var i = 0; i < tx.outputs.length; i++) - delete this._unspent[tx.hash('hex') + '/' + i]; + for (var i = 0; i < tx.outputs.length; i++) { + key = tx.hash('hex') + '/' + i; + if (this._unspent[key]) { + delete this._unspent[key]; + this._removeOutput(tx, i); + } + } if (!this._storage || noWrite) return; @@ -246,21 +273,123 @@ TXPool.prototype.prune = function prune(pruneOrphans) { this._orphans = {}; }; -TXPool.prototype.getAll = function getAll() { +TXPool.prototype.getAll = function getAll(address) { + if (!address) + address = this._wallet; + return Object.keys(this._all).map(function(key) { return this._all[key]; }, this).filter(function(tx) { - return this._wallet.ownOutput(tx) - || this._wallet.ownInput(tx); - }, this); + return address.ownOutput(tx) + || address.ownInput(tx); + }); +}; + +TXPool.prototype._addOutput = function _addOutput(tx, i, remove) { + var i, data, address, addr, output; + + output = tx.outputs[i]; + data = bcoin.script.getOutputData(output.script); + + if (!this._wallet.ownOutput(tx, i)) + return; + + if (data.scriptAddress) + data.addresses = [data.scriptAddress]; + + for (i = 0; i < data.addresses.length; i++) { + addr = data.addresses[i]; + if (!this._addresses[addr]) { + this._addresses[addr] = { + received: new bn(0), + sent: new bn(0), + balance: new bn(0) + }; + } + if (!remove) { + this._addresses[addr].balance.iadd(output.value); + this._addresses[addr].received.iadd(output.value); + } else { + this._addresses[addr].balance.isub(output.value); + this._addresses[addr].received.isub(output.value); + } + } + + if (!remove) { + this._balance.iadd(output.value); + this._received.iadd(output.value); + } else { + this._balance.isub(output.value); + this._received.isub(output.value); + } +}; + +TXPool.prototype._removeOutput = function _removeOutput(tx, i) { + return this._addOutput(tx, i, true); +}; + +TXPool.prototype._addInput = function _addInput(tx, i, remove) { + var i, input, prev, data, address, addr, output; + + input = tx.inputs[i]; + assert(input.prevout.tx); + + if (!this._wallet.ownOutput(input.prevout.tx, input.prevout.index)) + return; + + prev = input.prevout.tx.outputs[input.prevout.index]; + data = bcoin.script.getInputData(input.script, prev.script); + + if (data.scriptAddress) + data.addresses = [data.scriptAddress]; + + for (i = 0; i < data.addresses.length; i++) { + addr = data.addresses[i]; + if (!this._addresses[addr]) { + this._addresses[addr] = { + received: new bn(0), + sent: new bn(0), + balance: new bn(0) + }; + } + if (!remove) { + this._addresses[addr].balance.isub(prev.value); + this._addresses[addr].sent.iadd(prev.value); + } else { + this._addresses[addr].balance.iadd(prev.value); + this._addresses[addr].sent.isub(prev.value); + } + } + + if (!remove) { + this._balance.isub(prev.value); + this._sent.iadd(prev.value); + } else { + this._balance.iadd(prev.value); + this._sent.isub(prev.value); + } +}; + +TXPool.prototype._removeInput = function _removeInput(tx, i) { + return this._addInput(tx, i, true); +}; + +TXPool.prototype.getAddressBalance = function getAddressBalance(address) { + if (this._addresses[address]) + return this._addresses[address].balance.clone(); + + return new bn(0); }; -TXPool.prototype.getUnspent = function getUnspent() { +TXPool.prototype.getUnspent = function getUnspent(address) { + if (!address) + address = this._wallet; + return Object.keys(this._unspent).map(function(key) { return this._unspent[key]; }, this).filter(function(item) { - return this._wallet.ownOutput(item.tx, item.index); - }, this); + return address.ownOutput(item.tx, item.index); + }); }; TXPool.prototype.getPending = function getPending() { @@ -271,9 +400,9 @@ TXPool.prototype.getPending = function getPending() { }); }; -TXPool.prototype.getBalance = function getBalance() { +TXPool.prototype.getBalance = function getBalance(address) { var acc = new bn(0); - var unspent = this.getUnspent(); + var unspent = this.getUnspent(address); if (unspent.length === 0) return acc; @@ -282,6 +411,12 @@ TXPool.prototype.getBalance = function getBalance() { }, acc); }; +TXPool.prototype.getBalance = function getBalance(address) { + if (address) + return this.getAddressBalance(address); + return this._balance.clone(); +}; + // Legacy TXPool.prototype.all = TXPool.prototype.getAll; TXPool.prototype.unspent = TXPool.prototype.getUnspent; @@ -321,7 +456,7 @@ TXPool.fromJSON = function fromJSON(wallet, json) { }); }); - return tx; + return txPool; }; /** diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index f7b8b53fc..7532b2ee7 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -31,10 +31,12 @@ function Wallet(options) { options = utils.merge({}, options); - if (options.hd) { - options.master = options.hd !== true - ? bcoin.hd.privateKey(options.hd) - : bcoin.hd.privateKey(); + options.hd = options.hd !== false; + + if (options.hd && !options.master) { + options.master = options.hd === true + ? bcoin.hd.privateKey() + : bcoin.hd.privateKey(options.hd); delete options.hd; } @@ -47,24 +49,16 @@ function Wallet(options) { if (options.pub) options.publicKey = options.pub; - if (options.privateKey - || options.publicKey - || options.pair - || options.personalization - || options.entropy - || options.passphrase - || options.compressed) { - if ((options.pair instanceof bcoin.hd.privateKey) - || options.pair instanceof bcoin.hd.publicKey) { - options.master = options.pair; - delete options.pair; - } else if (options.privateKey instanceof bcoin.hd.privateKey) { - options.master = options.privateKey; - delete options.privateKey; - } else if (options.publicKey instanceof bcoin.hd.publicKey) { - options.master = options.publicKey; - delete options.publicKey; - } + if ((options.pair instanceof bcoin.hd.privateKey) + || (options.pair instanceof bcoin.hd.publicKey)) { + options.master = options.pair; + delete options.pair; + } else if (options.privateKey instanceof bcoin.hd.privateKey) { + options.master = options.privateKey; + delete options.privateKey; + } else if (options.publicKey instanceof bcoin.hd.publicKey) { + options.master = options.publicKey; + delete options.publicKey; } this.options = options; @@ -137,20 +131,11 @@ function Wallet(options) { if (!options.addresses) options.addresses = []; - if (options.privateKey - || options.publicKey - || options.pair - || options.personalization - || options.entropy - || options.passphrase - || options.compressed) { + if (this._isKeyOptions(options)) { options.addresses.unshift({ privateKey: options.privateKey, publicKey: options.publicKey, pair: options.pair, - personalization: options.personalization, - entropy: options.entropy, - compressed: options.compressed, type: this.type, subtype: this.subtype, m: this.m, @@ -189,11 +174,8 @@ function Wallet(options) { } else if (this.normal) { // Try to find the last receiving address if there is one. receiving = options.addresses.filter(function(address) { - return !address.change - && ((address.priv || address.privateKey) - || (address.pub || address.publicKey) - || (address.key || address.pair)); - }).pop(); + return !address.change && this._isKeyOptions(address); + }, this).pop(); if (receiving) { this.current = bcoin.address(receiving); } else { @@ -224,11 +206,38 @@ function Wallet(options) { inherits(Wallet, EventEmitter); +Wallet.prototype._pruneAddresses = function _pruneAddresses(options) { + var addresses = this.addresses.slice(); + var address; + + for (i = 0; i < addresses.length; i++) { + address = addresses[i]; + + if (address === this.current || address === this.changeAddress) + continue; + + if (!address.change) + continue; + + if (!address.derived) + continue; + + if (address.getBalance().cmpn(0) === 0) + this.removeAddress(address); + } +}; + +Wallet.prototype._isKeyOptions = function _isKeyOptions(options) { + return (options.priv || options.privateKey) + || (options.pub || options.publicKey) + || (options.key || options.pair); +}; + // Wallet ID: // bip45: Purpose key address // bip44: Account key address // normal: Address of first key in wallet -Wallet.prototype.getID = function() { +Wallet.prototype.getID = function getID() { if (this.bip45) return bcoin.address.key2addr(this.purposeKey.publicKey); @@ -244,7 +253,7 @@ Wallet.prototype.getID = function() { assert(false); }; -Wallet.prototype._initAddresses = function() { +Wallet.prototype._initAddresses = function _initAddresses() { var options = this.options; var i; @@ -438,6 +447,7 @@ Wallet.prototype._init = function init() { if (this.tx._loaded) { this.loading = false; + this._pruneAddresses(); return; } @@ -458,6 +468,7 @@ Wallet.prototype._init = function init() { self.current = self.createAddress(); if (self.changeAddress.ownOutput(tx)) self.changeAddress = self.createAddress(true); + self._pruneAddresses(); } self.emit('tx', tx); }); @@ -465,6 +476,7 @@ Wallet.prototype._init = function init() { this.tx.once('load', function(ts) { self.loading = false; self.lastTs = ts; + self._pruneAddresses(); self.emit('load', ts); }); diff --git a/test/hd-test.js b/test/hd-test.js index ac7d85345..10feb094b 100644 --- a/test/hd-test.js +++ b/test/hd-test.js @@ -85,6 +85,11 @@ describe('HD', function() { master.hdpub._unbuild(master.hdpub.xpubkey); }); + it('should deserialize and reserialize', function() { + var key = bcoin.hd.priv(); + assert.equal(bcoin.hd.fromJSON(key.toJSON()).xprivkey, key.xprivkey); + }); + it('should create an hd seed', function() { var seed = new bcoin.hd.seed({ // I have the same combination on my luggage: diff --git a/test/wallet-test.js b/test/wallet-test.js index dc0fcb555..8b0a523b0 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -115,25 +115,31 @@ describe('Wallet', function() { // Coinbase var t1 = bcoin.tx().out(w, 50000).out(w, 1000); + // balance: 51000 w.sign(t1); - var t2 = bcoin.tx().input(t1, 0) + var t2 = bcoin.tx().input(t1, 0) // 50000 .out(w, 24000) .out(w, 24000); + // balance: 49000 w.sign(t2); - var t3 = bcoin.tx().input(t1, 1) - .input(t2, 0) + var t3 = bcoin.tx().input(t1, 1) // 1000 + .input(t2, 0) // 24000 .out(w, 23000); + // balance: 47000 w.sign(t3); - var t4 = bcoin.tx().input(t2, 1) - .input(t3, 0) + var t4 = bcoin.tx().input(t2, 1) // 24000 + .input(t3, 0) // 23000 .out(w, 11000) .out(w, 11000); + // balance: 22000 w.sign(t4); - var f1 = bcoin.tx().input(t4, 1) + var f1 = bcoin.tx().input(t4, 1) // 11000 .out(f, 10000); + // balance: 11000 w.sign(f1); - var fake = bcoin.tx().input(t1, 1) + var fake = bcoin.tx().input(t1, 1) // 1000 (already redeemed) .out(w, 500); + // balance: 11000 // Just for debugging t1.hint = 't1';