scanning.

This commit is contained in:
Christopher Jeffrey 2016-06-26 10:57:48 -07:00
parent 5095f78dbf
commit a95ab18379
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
10 changed files with 191 additions and 66 deletions

View File

@ -126,8 +126,12 @@ Address.prototype.toScript = function toScript() {
* @returns {Base58String}
*/
Address.prototype.toString = function toString() {
return this.toBase58();
Address.prototype.toString = function toString(enc) {
if (enc === 'hex')
return this.getHash('hex');
if (enc === 'base58')
enc = null;
return this.toBase58(enc);
};
/**

View File

@ -175,7 +175,7 @@ Mnemonic.prototype.toSeed = function toSeed() {
if (!this.phrase)
this.phrase = this.createMnemonic();
this.seed = utils.pbkdf2(
this.seed = utils.pbkdf2Sync(
nfkd(this.phrase),
nfkd('mnemonic' + this.passphrase),
2048, 64, 'sha512');
@ -1083,9 +1083,9 @@ HDPrivateKey.prototype.fromBase58 = function fromBase58(xkey) {
HDPrivateKey.prototype.fromRaw = function fromRaw(raw) {
var p = new BufferReader(raw);
var i, type, prefix;
var i, version, type, prefix;
this.version = p.readU32BE();
version = p.readU32BE();
this.depth = p.readU8();
this.parentFingerPrint = p.readBytes(4);
this.childIndex = p.readU32BE();
@ -1097,7 +1097,7 @@ HDPrivateKey.prototype.fromRaw = function fromRaw(raw) {
for (i = 0; i < network.types.length; i++) {
type = network.types[i];
prefix = network[type].prefixes.xprivkey;
if (this.version === prefix)
if (version === prefix)
break;
}
@ -1616,9 +1616,9 @@ HDPublicKey.prototype.fromBase58 = function fromBase58(xkey) {
HDPublicKey.prototype.fromRaw = function fromRaw(raw) {
var p = new BufferReader(raw);
var i, type, prefix;
var i, version, type, prefix;
this.version = p.readU32BE();
version = p.readU32BE();
this.depth = p.readU8();
this.parentFingerPrint = p.readBytes(4);
this.childIndex = p.readU32BE();
@ -1629,7 +1629,7 @@ HDPublicKey.prototype.fromRaw = function fromRaw(raw) {
for (i = 0; i < network.types.length; i++) {
type = network.types[i];
prefix = network[type].prefixes.xpubkey;
if (this.version === prefix)
if (version === prefix)
break;
}

View File

@ -776,7 +776,7 @@ MTX.prototype.maxSize = function maxSize(options, force) {
options = {};
if (options instanceof bcoin.wallet)
options = { wallet: options, m: options.m, n: options.n };
options = { wallet: options };
if (options.wallet)
wallet = options.wallet;
@ -791,6 +791,7 @@ MTX.prototype.maxSize = function maxSize(options, force) {
if (!wallet)
return;
// Hack
address = wallet.receiveAddress;
if (address.program && hash.length === 20)

View File

@ -33,7 +33,6 @@
'use strict';
var crypto = require('crypto');
var utils = require('./utils');
/**
@ -70,7 +69,7 @@ function scrypt(passwd, salt, N, r, p, len, callback) {
XY = new Buffer(256 * r);
V = new Buffer(128 * r * N);
crypto.pbkdf2(passwd, salt, 1, p * 128 * r, 'sha256', function(err, B) {
utils.pbkdf2(passwd, salt, 1, p * 128 * r, 'sha256', function(err, B) {
if (err)
return callback(err);
@ -80,7 +79,7 @@ function scrypt(passwd, salt, N, r, p, len, callback) {
if (err)
return callback(err);
crypto.pbkdf2(passwd, B, 1, len, 'sha256', callback);
utils.pbkdf2(passwd, B, 1, len, 'sha256', callback);
});
});
}

View File

@ -69,12 +69,12 @@ function scrypt(passwd, salt, N, r, p, len) {
XY = new Buffer(256 * r);
V = new Buffer(128 * r * N);
B = utils.pbkdf2(passwd, salt, 1, p * 128 * r, 'sha256');
B = utils.pbkdf2Sync(passwd, salt, 1, p * 128 * r, 'sha256');
for (i = 0; i < p; i++)
smix(B, i * 128 * r, r, N, V, XY);
return utils.pbkdf2(passwd, B, 1, len, 'sha256');
return utils.pbkdf2Sync(passwd, B, 1, len, 'sha256');
}
function salsa20_8(B) {

View File

@ -201,15 +201,61 @@ WalletMap.prototype.getPaths = function getPaths(address) {
return this.table[address];
};
WalletMap.prototype.toJSON = function toJSON() {
return {
inputs: this.inputs.map(function(input) {
return input.toJSON();
}),
outputs: this.outputs.map(function(output) {
return output.toJSON();
}),
paths: this.paths.map(function(path) {
return path.toJSON();
}),
accounts: this.accounts.map(function(path) {
return {
id: path.id,
name: path.name,
account: path.account
};
}),
wallets: this.wallets
};
};
function MapMember() {
this.value = 0;
this.hash = null;
this.wallets = [];
this.address = null;
this.paths = [];
this.accounts = [];
this.wallets = [];
}
MapMember.prototype.toJSON = function toJSON() {
return {
value: utils.btc(this.value),
address: this.address
? this.address.toBase58()
: null,
hash: this.address
? this.address.getHash('hex')
: null,
paths: this.paths.map(function(path) {
return path.toJSON();
}),
accounts: this.accounts.map(function(path) {
return {
id: path.id,
name: path.name,
account: path.account
};
}),
wallets: this.wallets
};
};
MapMember.fromMember = function fromMember(table, io) {
var hash = io.getHash('hex');
var address = io.getAddress();
var member = new MapMember();
var i, paths;
@ -217,19 +263,20 @@ MapMember.fromMember = function fromMember(table, io) {
? io.coin.value
: io.value || 0;
if (!hash)
if (!address)
return member;
paths = table[hash];
paths = table[address.getHash('hex')];
assert(paths);
member.hash = hash;
member.address = address;
member.paths = paths;
for (i = 0; i < paths.length; i++)
member.wallets.push(paths[i].id);
member.accounts = uniq(member.paths);
member.wallets = utils.uniq(member.wallets);
return member;
@ -258,7 +305,7 @@ TXDB.prototype.getMap = function getMap(tx, callback) {
map = WalletMap.fromTX(table, tx);
utils.print(map);
utils.print(map.toJSON());
return callback(null, map);
});
@ -276,7 +323,7 @@ TXDB.prototype.mapAddresses = function mapAddresses(address, callback) {
var count = 0;
var i, keys, values;
return utils.forEachSerial(address, function(address, next) {
utils.forEachSerial(address, function(address, next) {
self.walletdb.getAddress(address, function(err, paths) {
if (err)
return next(err);

View File

@ -358,7 +358,7 @@ utils.hmac = function hmac(alg, data, salt) {
* @returns {Buffer}
*/
utils.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) {
utils.pbkdf2Sync = function pbkdf2Sync(key, salt, iter, len, alg) {
if (typeof key === 'string')
key = new Buffer(key, 'utf8');
@ -381,7 +381,7 @@ utils.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) {
* @param {Function} callback
*/
utils.pbkdf2Async = function pbkdf2Async(key, salt, iter, len, alg, callback) {
utils.pbkdf2 = function pbkdf2(key, salt, iter, len, alg, callback) {
var result;
if (typeof key === 'string')
@ -403,13 +403,13 @@ utils.pbkdf2Async = function pbkdf2Async(key, salt, iter, len, alg, callback) {
};
/**
* Derive a key using pbkdf2 with 25,000 iterations.
* Derive a key using pbkdf2 with 50,000 iterations.
* @param {Buffer|String} passphrase
* @param {Function} callback
*/
utils.derive = function derive(passphrase, callback) {
utils.pbkdf2Async(passphrase, 'bcoin', 25000, 32, 'sha256', callback);
utils.pbkdf2(passphrase, 'bcoin', 50000, 32, 'sha256', callback);
};
/**
@ -423,6 +423,7 @@ utils.derive = function derive(passphrase, callback) {
utils.encrypt = function encrypt(data, passphrase, iv, callback) {
assert(Buffer.isBuffer(data));
assert(passphrase, 'No passphrase.');
assert(Buffer.isBuffer(iv));
utils.derive(passphrase, function(err, key) {
if (err)
@ -474,6 +475,7 @@ utils.encipher = function encipher(data, key, iv) {
utils.decrypt = function decrypt(data, passphrase, iv, callback) {
assert(Buffer.isBuffer(data));
assert(passphrase, 'No passphrase.');
assert(Buffer.isBuffer(iv));
utils.derive(passphrase, function(err, key) {
if (err)

View File

@ -295,7 +295,7 @@ Wallet.prototype.lock = function lock() {
*/
Wallet.prototype.unlock = function unlock(passphrase, timeout, callback) {
this.master.toKey(passphrase, timeout, callback);
this.master.unlock(passphrase, timeout, callback);
};
/**
@ -340,7 +340,7 @@ Wallet.prototype.createAccount = function createAccount(options, callback, force
callback = utils.wrap(callback, unlock);
this.master.toKey(options.passphrase, options.timeout, function(err, master) {
this.unlock(options.passphrase, options.timeout, function(err, master) {
if (err)
return callback(err);
@ -918,19 +918,26 @@ Wallet.prototype.getRedeem = function getRedeem(hash, callback) {
/**
* Scan for active accounts and addresses. Used for importing a wallet.
* @param {Function} getByAddress - Must be a function which accepts
* @param {Function} scanner - Must be a function which accepts
* a {@link Base58Address} as well as a callback and returns
* transactions by address.
* @param {Function} callback - Return [Error, Number] (total number
* of addresses allocated).
*/
Wallet.prototype.scan = function scan(getByAddress, callback) {
Wallet.prototype.scan = function scan(maxGap, scanner, callback) {
var self = this;
var total = 0;
var index = 0;
var unlock;
unlock = this.locker.lock(scan, [getByAddress, callback]);
if (typeof maxGap === 'function') {
callback = scanner;
scanner = maxGap;
maxGap = null;
}
unlock = this.locker.lock(scan, [maxGap, scanner, callback]);
if (!unlock)
return;
@ -940,22 +947,24 @@ Wallet.prototype.scan = function scan(getByAddress, callback) {
if (!this.initialized)
return callback(new Error('Wallet is not initialized.'));
(function next(err, account) {
if (err)
return callback(err);
account.scan(getByAddress, function(err, result) {
(function next() {
self.getAccount(index++, function(err, account) {
if (err)
return callback(err);
if (result === 0)
if (!account)
return callback(null, total);
total += result;
account.scan(maxGap, scanner, function(err, result) {
if (err)
return callback(err);
self.createAccount(self.options, next, true);
});
})(null, this.account);
total += result;
next();
});
}, true);
})();
};
/**
@ -1012,7 +1021,7 @@ Wallet.prototype.sign = function sign(tx, options, callback) {
if (err)
return callback(err);
self.master.toKey(options.passphrase, options.timeout, function(err, master) {
self.unlock(options.passphrase, options.timeout, function(err, master) {
if (err)
return callback(err);
@ -1545,7 +1554,7 @@ function Account(db, options) {
this.db = db;
this.network = db.network;
this.lookahead = Account.LOOKAHEAD;
this.lookahead = Account.MAX_LOOKAHEAD;
this.loaded = false;
this.loading = false;
@ -1636,7 +1645,14 @@ Account.fromOptions = function fromOptions(db, options) {
* @const {Number}
*/
Account.LOOKAHEAD = 5;
Account.MAX_LOOKAHEAD = 5;
/*
* Default address gap for scanning.
* @const {Number}
*/
Account.MAX_GAP = 20;
/**
* Attempt to intialize the account (generating
@ -2040,16 +2056,25 @@ Account.prototype.setDepth = function setDepth(receiveDepth, changeDepth, callba
/**
* Scan for addresses.
* @param {Function} getByAddress - Must be a callback which accepts
* @param {Function} scanner - Must be a callback which accepts
* a callback and returns transactions by address.
* @param {Function} callback - Return [Error, Number] (total number
* of addresses allocated).
*/
Account.prototype.scan = function scan(getByAddress, callback) {
Account.prototype.scan = function scan(maxGap, scanner, callback) {
var self = this;
var total = 0;
if (typeof maxGap === 'function') {
callback = scanner;
scanner = maxGap;
maxGap = null;
}
if (maxGap == null)
maxGap = Account.MAX_GAP;
if (!this.initialized)
return callback(new Error('Account is not initialized.'));
@ -2068,37 +2093,63 @@ Account.prototype.scan = function scan(getByAddress, callback) {
}
(function chainCheck(change) {
var address = change ? self.changeAddress : self.receiveAddress;
var depth = change ? self.changeDepth : self.receiveDepth;
var index = 0;
var gap = 0;
(function next(err, address) {
if (err)
return callback(err);
function createAddress(callback) {
if (index === depth)
return self.createAddress(change, callback);
return callback(null, self.deriveAddress(change, index++));
}
getByAddress(address.getAddress(), function(err, txs) {
(function next() {
createAddress(function(err, address) {
if (err)
return callback(err);
addTX(txs, function(err, result) {
scanner(address.getAddress(), function(err, txs) {
if (err)
return callback(err);
if (result) {
total++;
gap = 0;
return self.createAddress(change, next);
}
addTX(txs, function(err, result) {
if (err)
return callback(err);
if (++gap < 20)
return self.createAddress(change, next);
// Special case for maxGap=0
if (maxGap === 0 && index === depth) {
if (!change)
return chainCheck(true);
return callback(null, total);
}
if (!change)
return chainCheck(true);
if (result) {
total++;
gap = 0;
return next();
}
return callback(null, total);
if (++gap < Account.MAX_GAP)
return next();
if (!change) {
self.receiveDepth = Math.max(depth, self.receiveDepth - gap);
self.receiveAddress = self.deriveReceive(self.receiveDepth - 1);
return chainCheck(true);
}
self.changeDepth = Math.max(depth, self.changeDepth - gap);
self.changeAddress = self.deriveChange(self.changeDepth - 1);
self.save(function(err) {
if (err)
return callback(err);
return callback(null, total);
});
});
});
});
})(null, address);
})();
})(false);
};
@ -2353,11 +2404,11 @@ MasterKey.fromOptions = function fromOptions(options) {
* @returns {HDPrivateKey}
*/
MasterKey.prototype.toKey = function toKey(passphrase, timeout, callback) {
MasterKey.prototype.unlock = function _unlock(passphrase, timeout, callback) {
var self = this;
var unlock;
unlock = this.locker.lock(toKey, [passphrase, timeout, callback]);
unlock = this.locker.lock(_unlock, [passphrase, timeout, callback]);
if (!unlock)
return;
@ -2538,6 +2589,16 @@ MasterKey.prototype.toRaw = function toRaw(writer) {
p.writeVarBytes(this.iv);
p.writeVarBytes(this.ciphertext);
// Future-proofing:
// algorithm (0=pbkdf2, 1=scrypt)
p.writeU8(0);
// iterations (pbkdf2) / N (scrypt)
p.writeU32(50000);
// r (scrypt)
p.writeU32(0);
// p (scrypt)
p.writeU32(0);
if (!writer)
p = p.render();

View File

@ -1211,6 +1211,16 @@ Path.prototype.inspect = function() {
+ '>';
};
Path.prototype.toJSON = function toJSON() {
return {
id: this.id,
name: this.name,
account: this.account,
change: this.change,
index: this.index
};
};
Path.prototype.toRaw = function toRaw(writer) {
var p = new BufferWriter(writer);

View File

@ -873,6 +873,7 @@ describe('Wallet', function() {
assert.ifError(err);
w1.master.stop();
w1.master.key = null;
utils.print(w1.toJSON());
// Coinbase
var t1 = bcoin.mtx()