hd keys. address pruning. txpool balance.

This commit is contained in:
Christopher Jeffrey 2016-02-04 18:58:56 -08:00
parent b9149a0855
commit b835fff86c
6 changed files with 271 additions and 63 deletions

View File

@ -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();

View File

@ -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
*/

View File

@ -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.getUnspent = function getUnspent() {
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(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;
};
/**

View File

@ -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);
});

View File

@ -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:

View File

@ -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';