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

View File

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

View File

@ -33,7 +33,6 @@
'use strict'; 'use strict';
var crypto = require('crypto');
var utils = require('./utils'); var utils = require('./utils');
/** /**
@ -70,7 +69,7 @@ function scrypt(passwd, salt, N, r, p, len, callback) {
XY = new Buffer(256 * r); XY = new Buffer(256 * r);
V = new Buffer(128 * r * N); 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) if (err)
return callback(err); return callback(err);
@ -80,7 +79,7 @@ function scrypt(passwd, salt, N, r, p, len, callback) {
if (err) if (err)
return callback(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); XY = new Buffer(256 * r);
V = new Buffer(128 * r * N); 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++) for (i = 0; i < p; i++)
smix(B, i * 128 * r, r, N, V, XY); 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) { function salsa20_8(B) {

View File

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

View File

@ -358,7 +358,7 @@ utils.hmac = function hmac(alg, data, salt) {
* @returns {Buffer} * @returns {Buffer}
*/ */
utils.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) { utils.pbkdf2Sync = function pbkdf2Sync(key, salt, iter, len, alg) {
if (typeof key === 'string') if (typeof key === 'string')
key = new Buffer(key, 'utf8'); key = new Buffer(key, 'utf8');
@ -381,7 +381,7 @@ utils.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) {
* @param {Function} callback * @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; var result;
if (typeof key === 'string') 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 {Buffer|String} passphrase
* @param {Function} callback * @param {Function} callback
*/ */
utils.derive = function derive(passphrase, 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) { utils.encrypt = function encrypt(data, passphrase, iv, callback) {
assert(Buffer.isBuffer(data)); assert(Buffer.isBuffer(data));
assert(passphrase, 'No passphrase.'); assert(passphrase, 'No passphrase.');
assert(Buffer.isBuffer(iv));
utils.derive(passphrase, function(err, key) { utils.derive(passphrase, function(err, key) {
if (err) if (err)
@ -474,6 +475,7 @@ utils.encipher = function encipher(data, key, iv) {
utils.decrypt = function decrypt(data, passphrase, iv, callback) { utils.decrypt = function decrypt(data, passphrase, iv, callback) {
assert(Buffer.isBuffer(data)); assert(Buffer.isBuffer(data));
assert(passphrase, 'No passphrase.'); assert(passphrase, 'No passphrase.');
assert(Buffer.isBuffer(iv));
utils.derive(passphrase, function(err, key) { utils.derive(passphrase, function(err, key) {
if (err) if (err)

View File

@ -295,7 +295,7 @@ Wallet.prototype.lock = function lock() {
*/ */
Wallet.prototype.unlock = function unlock(passphrase, timeout, callback) { 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); 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) if (err)
return callback(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. * 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 * a {@link Base58Address} as well as a callback and returns
* transactions by address. * transactions by address.
* @param {Function} callback - Return [Error, Number] (total number * @param {Function} callback - Return [Error, Number] (total number
* of addresses allocated). * of addresses allocated).
*/ */
Wallet.prototype.scan = function scan(getByAddress, callback) { Wallet.prototype.scan = function scan(maxGap, scanner, callback) {
var self = this; var self = this;
var total = 0; var total = 0;
var index = 0;
var unlock; 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) if (!unlock)
return; return;
@ -940,22 +947,24 @@ Wallet.prototype.scan = function scan(getByAddress, callback) {
if (!this.initialized) if (!this.initialized)
return callback(new Error('Wallet is not initialized.')); return callback(new Error('Wallet is not initialized.'));
(function next(err, account) { (function next() {
if (err) self.getAccount(index++, function(err, account) {
return callback(err);
account.scan(getByAddress, function(err, result) {
if (err) if (err)
return callback(err); return callback(err);
if (result === 0) if (!account)
return callback(null, total); return callback(null, total);
total += result; account.scan(maxGap, scanner, function(err, result) {
if (err)
return callback(err);
self.createAccount(self.options, next, true); total += result;
});
})(null, this.account); next();
});
}, true);
})();
}; };
/** /**
@ -1012,7 +1021,7 @@ Wallet.prototype.sign = function sign(tx, options, callback) {
if (err) if (err)
return callback(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) if (err)
return callback(err); return callback(err);
@ -1545,7 +1554,7 @@ function Account(db, options) {
this.db = db; this.db = db;
this.network = db.network; this.network = db.network;
this.lookahead = Account.LOOKAHEAD; this.lookahead = Account.MAX_LOOKAHEAD;
this.loaded = false; this.loaded = false;
this.loading = false; this.loading = false;
@ -1636,7 +1645,14 @@ Account.fromOptions = function fromOptions(db, options) {
* @const {Number} * @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 * Attempt to intialize the account (generating
@ -2040,16 +2056,25 @@ Account.prototype.setDepth = function setDepth(receiveDepth, changeDepth, callba
/** /**
* Scan for addresses. * 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. * a callback and returns transactions by address.
* @param {Function} callback - Return [Error, Number] (total number * @param {Function} callback - Return [Error, Number] (total number
* of addresses allocated). * of addresses allocated).
*/ */
Account.prototype.scan = function scan(getByAddress, callback) { Account.prototype.scan = function scan(maxGap, scanner, callback) {
var self = this; var self = this;
var total = 0; var total = 0;
if (typeof maxGap === 'function') {
callback = scanner;
scanner = maxGap;
maxGap = null;
}
if (maxGap == null)
maxGap = Account.MAX_GAP;
if (!this.initialized) if (!this.initialized)
return callback(new Error('Account is not initialized.')); return callback(new Error('Account is not initialized.'));
@ -2068,37 +2093,63 @@ Account.prototype.scan = function scan(getByAddress, callback) {
} }
(function chainCheck(change) { (function chainCheck(change) {
var address = change ? self.changeAddress : self.receiveAddress; var depth = change ? self.changeDepth : self.receiveDepth;
var index = 0;
var gap = 0; var gap = 0;
(function next(err, address) { function createAddress(callback) {
if (err) if (index === depth)
return callback(err); 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) if (err)
return callback(err); return callback(err);
addTX(txs, function(err, result) { scanner(address.getAddress(), function(err, txs) {
if (err) if (err)
return callback(err); return callback(err);
if (result) { addTX(txs, function(err, result) {
total++; if (err)
gap = 0; return callback(err);
return self.createAddress(change, next);
}
if (++gap < 20) // Special case for maxGap=0
return self.createAddress(change, next); if (maxGap === 0 && index === depth) {
if (!change)
return chainCheck(true);
return callback(null, total);
}
if (!change) if (result) {
return chainCheck(true); 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); })(false);
}; };
@ -2353,11 +2404,11 @@ MasterKey.fromOptions = function fromOptions(options) {
* @returns {HDPrivateKey} * @returns {HDPrivateKey}
*/ */
MasterKey.prototype.toKey = function toKey(passphrase, timeout, callback) { MasterKey.prototype.unlock = function _unlock(passphrase, timeout, callback) {
var self = this; var self = this;
var unlock; var unlock;
unlock = this.locker.lock(toKey, [passphrase, timeout, callback]); unlock = this.locker.lock(_unlock, [passphrase, timeout, callback]);
if (!unlock) if (!unlock)
return; return;
@ -2538,6 +2589,16 @@ MasterKey.prototype.toRaw = function toRaw(writer) {
p.writeVarBytes(this.iv); p.writeVarBytes(this.iv);
p.writeVarBytes(this.ciphertext); 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) if (!writer)
p = p.render(); 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) { Path.prototype.toRaw = function toRaw(writer) {
var p = new BufferWriter(writer); var p = new BufferWriter(writer);

View File

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