diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..cf9f0044 --- /dev/null +++ b/.babelrc @@ -0,0 +1,9 @@ +{ + "presets": ["es2015"], + "plugins": [ + ["transform-runtime", { + "polyfill": true, + "regenerator": true + }] + ] +} diff --git a/.jshintrc b/.jshintrc index cf7ce01d..70a62818 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,5 +1,6 @@ { "bitwise": false, + "esversion": 6, "curly": false, "eqeqeq": true, "freeze": true, diff --git a/README.md b/README.md index dc08842e..ae980034 100644 --- a/README.md +++ b/README.md @@ -114,30 +114,19 @@ var miner = new bcoin.miner({ chain: chain, mempool: mempool }); // Open the miner (initialize the databases, etc). // Miner will implicitly call `open` on chain and mempool. -miner.open(function(err) { - if (err) - throw err; - +miner.open().then(function() { // Create a block "attempt". - miner.createBlock(function(err, attempt) { - if (err) - throw err; - - // Mine the block on the worker pool (use mine() for the master process) - attempt.mineAsync(function(err, block) { - if (err) - throw err; - - // Add the block to the chain - chain.add(block, function(err) { - if (err) - throw err; - - console.log('Added %s to the blockchain.', block.rhash); - console.log(block); - }); - }); - }); + return miner.createBlock(); +}).then(function(attempt) { + // Mine the block on the worker pool (use mine() for the master process) + return attempt.mineAsync(); +}).then(function(block) { + // Add the block to the chain + console.log('Adding %s to the blockchain.', block.rhash); + console.log(block); + return chain.add(block); +}).then(function() { + console.log('Added block!'); }); ``` @@ -157,10 +146,7 @@ var mempool = new bcoin.mempool({ chain: chain }); var pool = new bcoin.pool({ chain: chain, mempool: mempool, maxPeers: 8 }); // Open the pool (implicitly opens mempool and chain). -pool.open(function(err) { - if (err) - throw err; - +pool.open().then(function() { // Connect, start retrieving and relaying txs pool.connect(); @@ -204,10 +190,7 @@ var tpool = new bcoin.pool({ size: 8 }); -tpool.open(function(err) { - if (err) - throw err; - +tpool.open().then(function() { // Connect, start retrieving and relaying txs tpool.connect(); @@ -252,38 +235,29 @@ var pool = new bcoin.pool({ var walletdb = new bcoin.walletdb({ db: 'memory' }); -pool.open(function(err) { - if (err) - throw err; +pool.open().then(function() { + return walletdb.open(); +}).then(function() { + return walletdb.create(); +}).then(function(wallet) { + console.log('Created wallet with address %s', wallet.getAddress('base58')); - walletdb.open(function(err) { - if (err) - throw err; + // Add our address to the spv filter. + pool.watchAddress(wallet.getAddress()); - walletdb.create(function(err, wallet) { - if (err) - throw err; + // Connect, start retrieving and relaying txs + pool.connect(); - console.log('Created wallet with address %s', wallet.getAddress('base58')); + // Start the blockchain sync. + pool.startSync(); - // Add our address to the spv filter. - pool.watchAddress(wallet.getAddress()); + pool.on('tx', function(tx) { + wallet.addTX(tx); + }); - // Connect, start retrieving and relaying txs - pool.connect(); - - // Start the blockchain sync. - pool.startSync(); - - pool.on('tx', function(tx) { - wallet.addTX(tx); - }); - - wallet.on('balance', function(balance) { - console.log('Balance updated.'); - console.log(bcoin.utils.btc(balance.unconfirmed)); - }); - }); + wallet.on('balance', function(balance) { + console.log('Balance updated.'); + console.log(bcoin.utils.btc(balance.unconfirmed)); }); }); ``` @@ -310,10 +284,7 @@ node.on('error', function(err) { }); // Start the node -node.open(function(err) { - if (err) - throw err; - +node.open().then(function() { // Create a new wallet (or get an existing one with the same ID) var options = { id: 'mywallet', @@ -322,48 +293,33 @@ node.open(function(err) { type: 'pubkeyhash' }; - node.walletdb.create(options, function(err, wallet) { - if (err) - throw err; + return node.walletdb.create(options); +}).then(function(wallet) { + console.log('Created wallet with address: %s', wallet.getAddress('base58')); - console.log('Created wallet with address: %s', wallet.getAddress('base58')); + // Start syncing the blockchain + node.startSync(); - // Start syncing the blockchain - node.startSync(); - - // Wait for balance and send it to a new address. - wallet.once('balance', function(balance) { - // Create a transaction, fill - // it with coins, and sign it. - var options = { - subtractFee: true, - outputs: [{ - address: newReceiving, - value: balance.total - }] - }; - wallet.createTX(options, function(err, tx) { - if (err) - throw err; - - // Need to pass our passphrase back in to sign! - wallet.sign(tx, 'foo', function(err) { - if (err) - throw err; - - console.log('sending tx:'); - console.log(tx); - - node.sendTX(tx, function(err) { - if (err) { - // Could be a reject - // packet or a timeout. - return console.log(err); - } - console.log('tx sent!'); - }); - }); - }); + // Wait for balance and send it to a new address. + wallet.once('balance', function(balance) { + // Create a transaction, fill + // it with coins, and sign it. + var options = { + subtractFee: true, + outputs: [{ + address: newReceiving, + value: balance.total + }] + }; + wallet.createTX(options).then(function(tx) { + // Need to pass our passphrase back in to sign! + return wallet.sign(tx, 'foo'); + }).then(function(tx) { + console.log('sending tx:'); + console.log(tx); + return node.sendTX(tx); + }).then(function() { + console.log('tx sent!'); }); }); }); @@ -377,12 +333,7 @@ node.mempool.on('tx', function(tx) { }); node.chain.on('full', function() { - node.mempool.getHistory(function(err, txs) { - if (err) - throw err; - - console.log(txs); - }); + node.mempool.getHistory().then(console.log); }); ``` diff --git a/bench/coin.js b/bench/coin.js index 2009eca8..f2305104 100644 --- a/bench/coin.js +++ b/bench/coin.js @@ -1,20 +1,19 @@ 'use strict'; var bn = require('bn.js'); -var bcoin = require('../').set('main'); -var constants = bcoin.constants; -var utils = bcoin.utils; +var constants = require('../lib/protocol/constants'); +var utils = require('../lib/utils/utils'); var assert = require('assert'); var scriptTypes = constants.scriptTypes; var bench = require('./bench'); var fs = require('fs'); - -bcoin.cache(); +var Coins = require('../lib/chain/coins'); +var TX = require('../lib/primitives/tx'); var wtx = fs.readFileSync(__dirname + '/../test/data/wtx.hex', 'utf8'); -wtx = bcoin.tx.fromRaw(wtx.trim(), 'hex'); +wtx = tx.fromRaw(wtx.trim(), 'hex'); -var coins = bcoin.coins.fromTX(wtx); +var coins = Coins.fromTX(wtx); var raw; var end = bench('serialize'); @@ -24,16 +23,16 @@ end(i); var end = bench('parse'); for (var i = 0; i < 10000; i++) - bcoin.coins.fromRaw(raw); + Coins.fromRaw(raw); end(i); var end = bench('parse-single'); var hash = wtx.hash('hex'); for (var i = 0; i < 10000; i++) - bcoin.coins.parseCoin(raw, hash, 5); + Coins.parseCoin(raw, hash, 5); end(i); -var coins = bcoin.coins.fromRaw(raw); +var coins = Coins.fromRaw(raw); var end = bench('get'); var j; diff --git a/bench/walletdb.js b/bench/walletdb.js index e01f0e74..07b3ed28 100644 --- a/bench/walletdb.js +++ b/bench/walletdb.js @@ -7,6 +7,7 @@ var utils = bcoin.utils; var assert = require('assert'); var scriptTypes = constants.scriptTypes; var bench = require('./bench'); +var co = require('../lib/utils/co'); bcoin.cache(); @@ -35,115 +36,85 @@ var walletdb = new bcoin.walletdb({ // db: 'leveldb' db: 'memory' }); -var wallet; -var addrs = []; -function runBench(callback) { - utils.serial([ - function(next) { - walletdb.create(function(err, w) { - assert.ifError(err); - wallet = w; - next(); - }); - }, - function(next) { - var end = bench('accounts'); - utils.forRange(0, 1000, function(i, next) { - wallet.createAccount({}, function(err, account) { - assert.ifError(err); - addrs.push(account.receiveAddress.getAddress()); - next(); - }); - }, function(err) { - assert.ifError(err); - end(1000); - next(); - }); - }, - function(next) { - var end = bench('addrs'); - utils.forRange(0, 1000, function(i, next) { - utils.forRange(0, 10, function(j, next) { - wallet.createReceive(i, function(err, addr) { - assert.ifError(err); - addrs.push(addr); - next(); - }); - }, next); - }, function(err) { - assert.ifError(err); - end(1000 * 10); - next(); - }); - }, - function(next) { - var nonce = new bn(0); - var end; - utils.forRange(0, 10000, function(i, next) { - var t1 = bcoin.mtx() - .addOutput(addrs[(i + 0) % addrs.length], 50460) - .addOutput(addrs[(i + 1) % addrs.length], 50460) - .addOutput(addrs[(i + 2) % addrs.length], 50460) - .addOutput(addrs[(i + 3) % addrs.length], 50460); +var runBench = co(function* runBench() { + var i, j, wallet, addrs, jobs, end; + var result, nonce, tx, options; - t1.addInput(dummyInput); - nonce.addn(1); - t1.inputs[0].script.set(0, nonce); - t1.inputs[0].script.compile(); + // Open and Create + yield walletdb.open(); + wallet = yield walletdb.create(); + addrs = []; - walletdb.addTX(t1.toTX(), function(err) { - assert.ifError(err); - next(); - }); - }, function(err) { - assert.ifError(err); - end(10000); - next(); - }); - end = bench('tx'); - }, - function(next) { - var end = bench('balance'); - wallet.getBalance(function(err, balance) { - assert.ifError(err); - end(1); - next(); - }); - }, - function(next) { - var end = bench('coins'); - wallet.getCoins(function(err) { - assert.ifError(err); - end(1); - next(); - }); - }, - function(next) { - var end = bench('create'); - var options = { - rate: 10000, - outputs: [{ - value: 50460, - address: addrs[0] - }] - }; - wallet.createTX(options, function(err) { - assert.ifError(err); - end(1); - next(); - }); - } - ], function(err) { - assert.ifError(err); - callback(); - }); -} + // Accounts + jobs = []; + for (i = 0; i < 1000; i++) + jobs.push(wallet.createAccount({})); -walletdb.open(function(err) { - assert.ifError(err); - runBench(function(err) { - assert.ifError(err); - process.exit(0); - }); + end = bench('accounts'); + result = yield Promise.all(jobs); + end(1000); + + for (i = 0; i < result.length; i++) + addrs.push(result[i].receive.getAddress()); + + // Addresses + jobs = []; + for (i = 0; i < 1000; i++) { + for (j = 0; j < 10; j++) + jobs.push(wallet.createReceive(i)); + } + + end = bench('addrs'); + result = yield Promise.all(jobs); + end(1000 * 10); + + for (i = 0; i < result.length; i++) + addrs.push(result[i].getAddress()); + + // TX + jobs = []; + nonce = new bn(0); + for (i = 0; i < 10000; i++) { + tx = bcoin.mtx() + .addOutput(addrs[(i + 0) % addrs.length], 50460) + .addOutput(addrs[(i + 1) % addrs.length], 50460) + .addOutput(addrs[(i + 2) % addrs.length], 50460) + .addOutput(addrs[(i + 3) % addrs.length], 50460); + + tx.addInput(dummyInput); + nonce.addn(1); + tx.inputs[0].script.set(0, nonce); + tx.inputs[0].script.compile(); + + jobs.push(walletdb.addTX(tx.toTX())); + } + + end = bench('tx'); + result = yield Promise.all(jobs); + end(10000); + + // Balance + end = bench('balance'); + result = yield wallet.getBalance(); + end(1); + + // Coins + end = bench('coins'); + result = yield wallet.getCoins(); + end(1); + + // Create + end = bench('create'); + options = { + rate: 10000, + outputs: [{ + value: 50460, + address: addrs[0] + }] + }; + yield wallet.createTX(options); + end(1); }); + +runBench().then(process.exit); diff --git a/bin/cli b/bin/cli index 20631252..2f7f651f 100755 --- a/bin/cli +++ b/bin/cli @@ -4,9 +4,11 @@ var config = require('../lib/node/config'); var utils = require('../lib/utils/utils'); +var co = require('../lib/utils/co'); var Client = require('../lib/http/client'); var Wallet = require('../lib/http/wallet'); -var assert = utils.assert; +var assert = require('assert'); +var main; function CLI() { this.config = config({ @@ -26,9 +28,9 @@ CLI.prototype.log = function log(json) { console.log(JSON.stringify(json, null, 2)); }; -CLI.prototype.createWallet = function createWallet(callback) { - var self = this; +CLI.prototype.createWallet = co(function* createWallet() { var options = { id: this.argv[0] }; + var wallet; if (this.config.type) options.type = this.config.type; @@ -51,170 +53,118 @@ CLI.prototype.createWallet = function createWallet(callback) { if (this.config.passphrase) options.passphrase = this.config.passphrase; - this.client.createWallet(options, function(err, wallet) { - if (err) - return callback(err); - self.log(wallet); - callback(); - }); -}; + wallet = yield this.client.createWallet(options); + this.log(wallet); +}); -CLI.prototype.addKey = function addKey(callback) { - var self = this; +CLI.prototype.addKey = co(function* addKey() { var key = this.argv[0]; - this.wallet.addKey(this.config.account, key, function(err, wallet) { - if (err) - return callback(err); - self.log('added'); - callback(); - }); -}; + yield this.wallet.addKey(this.config.account, key); + this.log('added'); +}); -CLI.prototype.removeKey = function removeKey(callback) { - var self = this; +CLI.prototype.removeKey = co(function* removeKey() { var key = this.argv[0]; - this.wallet.removeKey(this.config.account, key, function(err) { - if (err) - return callback(err); - self.log('removed'); - callback(); - }); -}; + yield this.wallet.removeKey(this.config.account, key); + this.log('removed'); +}); -CLI.prototype.getAccount = function getAccount(callback) { - var self = this; +CLI.prototype.getAccount = co(function* getAccount() { var account = this.argv[0] || this.config.account; - this.wallet.getAccount(account, function(err, account) { - if (err) - return callback(err); - self.log(account); - callback(); - }); -}; + yield this.wallet.getAccount(account); + this.log(account); +}); -CLI.prototype.createAccount = function createAccount(callback) { - var self = this; +CLI.prototype.createAccount = co(function* createAccount() { + var name = this.argv[0]; + var account = yield this.wallet.createAccount(name); + this.log(account); +}); + +CLI.prototype.createAddress = co(function* createAddress() { var account = this.argv[0]; - this.wallet.createAccount(account, function(err, account) { - if (err) - return callback(err); - self.log(account); - callback(); - }); -}; + var addr = yield this.wallet.createAddress(account); + this.log(addr); +}); -CLI.prototype.createAddress = function createAddress(callback) { - var self = this; +CLI.prototype.createNested = co(function* createNested() { var account = this.argv[0]; - this.wallet.createAddress(account, function(err, account) { - if (err) - return callback(err); - self.log(account); - callback(); - }); -}; + var addr = yield this.wallet.createNested(account); + this.log(addr); +}); -CLI.prototype.getAccounts = function getAccounts(callback) { - var self = this; - this.wallet.getAccounts(function(err, accounts) { - if (err) - return callback(err); - self.log(accounts); - callback(); - }); -}; +CLI.prototype.getAccounts = co(function* getAccounts() { + var accounts = yield this.wallet.getAccounts(); + this.log(accounts); +}); -CLI.prototype.getWallet = function getWallet(callback) { - var self = this; - this.wallet.getInfo(function(err, wallet) { - if (err) - return callback(err); - self.log(wallet); - callback(); - }); -}; +CLI.prototype.getWallet = co(function* getWallet() { + var info = yield this.wallet.getInfo(); + this.log(info); +}); -CLI.prototype.getTX = function getTX(callback) { - var self = this; +CLI.prototype.getTX = co(function* getTX() { var hash = this.argv[0]; + var txs, tx; + if (utils.isBase58(hash)) { - return this.client.getTXByAddress(hash, function(err, txs) { - if (err) - return callback(err); - self.log(txs); - callback(); - }); + txs = yield this.client.getTXByAddress(hash); + this.log(txs); + return; } - this.client.getTX(hash, function(err, tx) { - if (err) - return callback(err); - if (!tx) { - self.log('TX not found.'); - return callback(); - } + tx = yield this.client.getTX(hash); - self.log(tx); - callback(); - }); -}; + if (!tx) { + this.log('TX not found.'); + return; + } -CLI.prototype.getBlock = function getBlock(callback) { - var self = this; + this.log(tx); +}); + +CLI.prototype.getBlock = co(function* getBlock() { var hash = this.argv[0]; if (hash.length !== 64) hash = +hash; - this.client.getBlock(hash, function(err, block) { - if (err) - return callback(err); - if (!block) { - self.log('Block not found.'); - return callback(); - } + block = yield this.client.getBlock(hash); - self.log(block); - callback(); - }); -}; + if (!block) { + this.log('Block not found.'); + return + } -CLI.prototype.getCoin = function getCoin(callback) { - var self = this; + this.log(block); +}); + +CLI.prototype.getCoin = co(function* getCoin() { var hash = this.argv[0]; var index = this.argv[1]; + var coins, coin; + if (utils.isBase58(hash)) { - return this.client.getCoinsByAddress(hash, function(err, coins) { - if (err) - return callback(err); - self.log(coins); - callback(); - }); + coins = yield this.client.getCoinsByAddress(hash); + this.log(coins); + return; } - this.client.getCoin(hash, index, function(err, coin) { - if (err) - return callback(err); - if (!coin) { - self.log('Coin not found.'); - return callback(); - } + coin = yield this.client.getCoin(hash, index); - self.log(coin); - callback(); - }); -}; + if (!coin) { + this.log('Coin not found.'); + return; + } -CLI.prototype.getWalletHistory = function getWalletHistory(callback) { - var self = this; - this.wallet.getHistory(this.config.account, function(err, txs) { - if (err) - return callback(err); - self.log(txs); - callback(); - }); -}; + this.log(coin); +}); -CLI.prototype.listenWallet = function listenWallet(callback) { +CLI.prototype.getWalletHistory = co(function* getWalletHistory() { + var txs = yield this.wallet.getHistory(this.config.account); + this.log(txs); +}); + +CLI.prototype.listenWallet = function listenWallet() { var self = this; this.wallet.on('tx', function(details) { self.log('TX:'); @@ -240,32 +190,22 @@ CLI.prototype.listenWallet = function listenWallet(callback) { self.log('Balance:'); self.log(balance); }); + return new Promise(function() {}); }; -CLI.prototype.getBalance = function getBalance(callback) { - var self = this; - this.wallet.getBalance(this.config.account, function(err, balance) { - if (err) - return callback(err); - self.log(balance); - callback(); - }); -}; +CLI.prototype.getBalance = co(function* getBalance() { + var balance = yield this.wallet.getBalance(this.config.account); + this.log(balance); +}); -CLI.prototype.getMempool = function getMempool(callback) { - var self = this; - this.client.getMempool(function(err, txs) { - if (err) - return callback(err); - self.log(txs); - callback(); - }); -}; +CLI.prototype.getMempool = co(function* getMempool() { + var txs = yield this.client.getMempool(); + this.log(txs); +}); -CLI.prototype.sendTX = function sendTX(callback) { - var self = this; +CLI.prototype.sendTX = co(function* sendTX() { var output = {}; - var options; + var options, tx; if (this.config.script) { output.script = this.config.script; @@ -281,18 +221,14 @@ CLI.prototype.sendTX = function sendTX(callback) { outputs: [output] }; - this.wallet.send(options, function(err, tx) { - if (err) - return callback(err); - self.log(tx); - callback(); - }); -}; + tx = yield this.wallet.send(options); -CLI.prototype.createTX = function createTX(callback) { - var self = this; + this.log(tx); +}); + +CLI.prototype.createTX = co(function* createTX() { var output = {}; - var options; + var options, tx; if (this.config.script) { output.script = this.config.script; @@ -308,86 +244,53 @@ CLI.prototype.createTX = function createTX(callback) { outputs: [output] }; - this.wallet.createTX(options, function(err, tx) { - if (err) - return callback(err); - self.log(tx); - callback(); - }); -}; + tx = yield this.wallet.createTX(options); -CLI.prototype.signTX = function signTX(callback) { - var self = this; + this.log(tx); +}); + +CLI.prototype.signTX = co(function* signTX() { var options = { passphrase: this.config.passphrase }; - var tx = options.tx || this.argv[0]; - this.wallet.sign(tx, options, function(err, tx) { - if (err) - return callback(err); - self.log(tx); - callback(); - }); -}; + var raw = options.tx || this.argv[0]; + var tx = yield this.wallet.sign(raw, options); + this.log(tx); +}); -CLI.prototype.zap = function zap(callback) { - var self = this; +CLI.prototype.zap = co(function* zap() { var age = (this.config.age >>> 0) || 72 * 60 * 60; - this.wallet.zap(this.config.account, age, function(err) { - if (err) - return callback(err); - self.log('Zapped!'); - callback(); - }); -}; + yield this.wallet.zap(this.config.account, age); + this.log('Zapped!'); +}); -CLI.prototype.broadcast = function broadcast(callback) { +CLI.prototype.broadcast = co(function* broadcast() { var self = this; - var tx = this.argv[0] || this.config.tx; - this.client.broadcast(tx, function(err, tx) { - if (err) - return callback(err); - self.log('Broadcasted:'); - self.log(tx); - callback(); - }); -}; + var raw = this.argv[0] || this.config.tx; + var tx = yield this.client.broadcast(raw); + this.log('Broadcasted:'); + this.log(tx); +}); -CLI.prototype.viewTX = function viewTX(callback) { - var self = this; - var tx = this.argv[0] || this.config.tx; - this.wallet.fill(tx, function(err, tx) { - if (err) - return callback(err); - self.log(tx); - callback(); - }); -}; +CLI.prototype.viewTX = co(function* viewTX() { + var raw = this.argv[0] || this.config.tx; + var tx = yield this.wallet.fill(raw); + this.log(tx); +}); -CLI.prototype.getDetails = function getDetails(callback) { - var self = this; +CLI.prototype.getDetails = co(function* getDetails() { var hash = this.argv[0]; - this.wallet.getTX(hash, function(err, tx) { - if (err) - return callback(err); - self.log(tx); - callback(); - }); -}; + var details = yield this.wallet.getTX(hash); + this.log(details); +}); -CLI.prototype.retoken = function retoken(callback) { - var self = this; - this.wallet.retoken(function(err, result) { - if (err) - return callback(err); - self.log(result); - callback(); - }); -}; +CLI.prototype.retoken = co(function* retoken() { + var result = yield this.wallet.retoken(); + this.log(result); +}); -CLI.prototype.rpc = function rpc(callback) { - var self = this; +CLI.prototype.rpc = co(function* rpc() { var method = this.argv.shift(); var params = []; - var i, arg, param; + var i, arg, param, result; for (i = 0; i < this.argv.length; i++) { arg = this.argv[i]; @@ -399,17 +302,12 @@ CLI.prototype.rpc = function rpc(callback) { params.push(param); } - this.client.rpc.call(method, params, function(err, result) { - if (err) - return callback(err); - self.log(result); - callback(); - }); -}; + result = yield this.client.rpc.call(method, params); -CLI.prototype.handleWallet = function handleWallet(callback) { - var self = this; + this.log(result); +}); +CLI.prototype.handleWallet = co(function* handleWallet() { var options = { id: this.config.id || 'primary', token: this.config.token @@ -421,81 +319,81 @@ CLI.prototype.handleWallet = function handleWallet(callback) { network: this.config.network }); - this.wallet.open(options, function(err) { - if (err) - return callback(err); + yield this.wallet.open(options); - switch (self.argv.shift()) { - case 'listen': - return self.listenWallet(callback); - case 'get': - return self.getWallet(callback); - case 'addkey': - return self.addKey(callback); - case 'rmkey': - return self.removeKey(callback); - case 'balance': - return self.getBalance(callback); - case 'history': - return self.getWalletHistory(callback); - case 'account': - if (self.argv[0] === 'list') { - self.argv.shift(); - return self.getAccounts(callback); - } - if (self.argv[0] === 'create') { - self.argv.shift(); - return self.createAccount(callback); - } - if (self.argv[0] === 'get') - self.argv.shift(); - return self.getAccount(callback); - case 'address': - return self.createAddress(callback); - case 'retoken': - return self.retoken(callback); - case 'sign': - return self.signTX(callback); - case 'mktx': - return self.createTX(callback); - case 'send': - return self.sendTX(callback); - case 'zap': - return self.zap(callback); - case 'tx': - return self.getDetails(callback); - case 'view': - return self.viewTX(callback); - default: - self.log('Unrecognized command.'); - self.log('Commands:'); - self.log(' $ listen: Listen for events.'); - self.log(' $ get: View wallet.'); - self.log(' $ addkey [xpubkey]: Add key to wallet.'); - self.log(' $ rmkey [xpubkey]: Remove key from wallet.'); - self.log(' $ balance: Get wallet balance.'); - self.log(' $ history: View wallet TX history.'); - self.log(' $ account list: List account names.'); - self.log(' $ account create [account-name]: Create account.'); - self.log(' $ account get [account-name]: Get account details.'); - self.log(' $ address: Derive new address.'); - self.log(' $ retoken: Create new api key.'); - self.log(' $ send [address] [value]: Send transaction.'); - self.log(' $ mktx [address] [value]: Create transaction.'); - self.log(' $ sign [tx-hex]: Sign transaction.'); - self.log(' $ zap --age [age]: Zap pending wallet TXs.'); - self.log(' $ tx [hash]: View transaction details.'); - self.log(' $ view [tx-hex]: Parse and view transaction.'); - self.log('Other Options:'); - self.log(' --passphrase [passphrase]: For signing and account creation.'); - self.log(' --account [account-name]: Account name.'); - return callback(); - } - }); -}; + switch (this.argv.shift()) { + case 'listen': + return yield this.listenWallet(); + case 'get': + return yield this.getWallet(); + case 'addkey': + return yield this.addKey(); + case 'rmkey': + return yield this.removeKey(); + case 'balance': + return yield this.getBalance(); + case 'history': + return yield this.getWalletHistory(); + case 'account': + if (this.argv[0] === 'list') { + this.argv.shift(); + return yield this.getAccounts(); + } + if (this.argv[0] === 'create') { + this.argv.shift(); + return yield this.createAccount(); + } + if (this.argv[0] === 'get') + this.argv.shift(); + return yield this.getAccount(); + case 'address': + return yield this.createAddress(); + case 'nested': + return yield this.createNested(); + case 'retoken': + return yield this.retoken(); + case 'sign': + return yield this.signTX(); + case 'mktx': + return yield this.createTX(); + case 'send': + return yield this.sendTX(); + case 'zap': + return yield this.zap(); + case 'tx': + return yield this.getDetails(); + case 'view': + return yield this.viewTX(); + default: + this.log('Unrecognized command.'); + this.log('Commands:'); + this.log(' $ listen: Listen for events.'); + this.log(' $ get: View wallet.'); + this.log(' $ addkey [xpubkey]: Add key to wallet.'); + this.log(' $ rmkey [xpubkey]: Remove key from wallet.'); + this.log(' $ balance: Get wallet balance.'); + this.log(' $ history: View wallet TX history.'); + this.log(' $ account list: List account names.'); + this.log(' $ account create [account-name]: Create account.'); + this.log(' $ account get [account-name]: Get account details.'); + this.log(' $ address: Derive new address.'); + this.log(' $ nested: Derive new nested address.'); + this.log(' $ retoken: Create new api key.'); + this.log(' $ send [address] [value]: Send transaction.'); + this.log(' $ mktx [address] [value]: Create transaction.'); + this.log(' $ sign [tx-hex]: Sign transaction.'); + this.log(' $ zap --age [age]: Zap pending wallet TXs.'); + this.log(' $ tx [hash]: View transaction details.'); + this.log(' $ view [tx-hex]: Parse and view transaction.'); + this.log('Other Options:'); + this.log(' --passphrase [passphrase]: For signing and account creation.'); + this.log(' --account [account-name]: Account name.'); + return; + } +}); -CLI.prototype.handleNode = function handleNode(callback) { - var self = this; +CLI.prototype.handleNode = co(function* handleNode() { + var info; this.client = new Client({ uri: this.config.url || this.config.uri, @@ -503,75 +401,67 @@ CLI.prototype.handleNode = function handleNode(callback) { network: this.config.network }); - this.client.getInfo(function(err, info) { - if (err) - return callback(err); + info = yield this.client.getInfo(); - switch (self.argv.shift()) { - case 'mkwallet': - return self.createWallet(callback); - case 'broadcast': - return self.broadcast(callback); - case 'mempool': - return self.getMempool(callback); - case 'tx': - return self.getTX(callback); - case 'coin': - return self.getCoin(callback); - case 'block': - return self.getBlock(callback); - case 'rpc': - return self.rpc(callback); - default: - self.log('Unrecognized command.'); - self.log('Commands:'); - self.log(' $ wallet create [id]: Create wallet.'); - self.log(' $ broadcast [tx-hex]: Broadcast transaction.'); - self.log(' $ mempool: Get mempool snapshot.'); - self.log(' $ tx [hash/address]: View transactions.'); - self.log(' $ coin [hash+index/address]: View coins.'); - self.log(' $ block [hash/height]: View block.'); - return callback(); - } - }); -}; + switch (this.argv.shift()) { + case 'mkwallet': + return yield this.createWallet(); + case 'broadcast': + return yield this.broadcast(); + case 'mempool': + return yield this.getMempool(); + case 'tx': + return yield this.getTX(); + case 'coin': + return yield this.getCoin(); + case 'block': + return yield this.getBlock(); + case 'rpc': + return yield this.rpc(); + default: + this.log('Unrecognized command.'); + this.log('Commands:'); + this.log(' $ wallet create [id]: Create wallet.'); + this.log(' $ broadcast [tx-hex]: Broadcast transaction.'); + this.log(' $ mempool: Get mempool snapshot.'); + this.log(' $ tx [hash/address]: View transactions.'); + this.log(' $ coin [hash+index/address]: View coins.'); + this.log(' $ block [hash/height]: View block.'); + this.log(' $ rpc [command] [args]: Execute RPC command.'); + return; + } +}); -CLI.prototype.open = function open(callback) { +CLI.prototype.open = co(function* open() { switch (this.argv[0]) { case 'w': case 'wallet': this.argv.shift(); if (this.argv[0] === 'create') { this.argv[0] = 'mkwallet'; - return this.handleNode(callback); + return yield this.handleNode(); } - return this.handleWallet(callback); + return yield this.handleWallet(); default: - return this.handleNode(callback); + return yield this.handleNode(); } -}; +}); -CLI.prototype.destroy = function destroy(callback) { +CLI.prototype.destroy = function destroy() { if (this.wallet && !this.wallet.client.loading) this.wallet.client.destroy(); if (this.client && !this.client.loading) this.client.destroy(); - callback(); + return Promise.resolve(null); }; -function main(callback) { +main = co(function* main() { var cli = new CLI(); - cli.open(function(err) { - if (err) - return callback(err); - cli.destroy(callback); - }); -} - -main(function(err) { - if (err) { - console.error(err.stack + ''); - return process.exit(1); - } - return process.exit(0); + yield cli.open(); + yield cli.destroy(); +}); + +main().then(process.exit).catch(function(err) { + console.error(err.stack + ''); + return process.exit(1); }); diff --git a/bin/node b/bin/node index 9307781e..b8050722 100755 --- a/bin/node +++ b/bin/node @@ -6,7 +6,7 @@ process.title = 'bcoin'; var bcoin = require('../'); var utils = bcoin.utils; -var assert = utils.assert; +var assert = require('assert'); var options = bcoin.config({ config: true, @@ -32,10 +32,7 @@ process.on('uncaughtException', function(err) { process.exit(1); }); -node.open(function(err) { - if (err) - throw err; - +node.open().then(function() { node.pool.connect(); node.startSync(); }); diff --git a/bin/spvnode b/bin/spvnode index c8f43091..a10abc42 100755 --- a/bin/spvnode +++ b/bin/spvnode @@ -6,7 +6,7 @@ process.title = 'bcoin'; var bcoin = require('../'); var utils = bcoin.utils; -var assert = utils.assert; +var assert = require('assert'); var options = bcoin.config({ config: true, @@ -25,10 +25,13 @@ node.on('error', function(err) { ; }); -node.open(function(err) { - if (err) - throw err; +process.on('uncaughtException', function(err) { + node.logger.debug(err.stack); + node.logger.error(err); + process.exit(1); +}); +node.open().then(function() { if (process.argv.indexOf('--test') !== -1) { node.pool.watchAddress('1VayNert3x1KzbpzMGt2qdqrAThiRovi8'); node.pool.watch(bcoin.outpoint().toRaw()); diff --git a/browser/index.html b/browser/index.html index 9f794b91..ea002924 100644 --- a/browser/index.html +++ b/browser/index.html @@ -32,7 +32,7 @@ margin-top: 10px; font: 1em monospace; } - .send { + .rpc, .send { padding: 5px; margin-left: 5px; margin-top: 10px; @@ -84,6 +84,10 @@ more bitcoin magic).
+
+ +
@@ -92,220 +96,6 @@ more bitcoin magic).
- + diff --git a/browser/index.js b/browser/index.js new file mode 100644 index 00000000..303ded75 --- /dev/null +++ b/browser/index.js @@ -0,0 +1,245 @@ +;(function() { + +'use strict'; + +var utils = bcoin.utils; +var body = document.getElementsByTagName('body')[0]; +var log = document.getElementById('log'); +var wdiv = document.getElementById('wallet'); +var tdiv = document.getElementById('tx'); +var floating = document.getElementById('floating'); +var send = document.getElementById('send'); +var newaddr = document.getElementById('newaddr'); +var chainState = document.getElementById('state'); +var rpc = document.getElementById('rpc'); +var cmd = document.getElementById('cmd'); +var items = []; +var scrollback = 0; +var logger, node, options; + +body.onmouseup = function() { + floating.style.display = 'none'; +}; + +floating.onmouseup = function(ev) { + ev.stopPropagation(); + return false; +}; + +function show(obj) { + floating.innerHTML = escape(utils.inspectify(obj, false)); + floating.style.display = 'block'; +} + +logger = new bcoin.logger({ level: 'debug' }); +logger.writeConsole = function(level, args) { + var msg = utils.format(args, false); + if (++scrollback > 1000) { + log.innerHTML = ''; + scrollback = 1; + } + log.innerHTML += '' + utils.now() + ' '; + if (level === 'error') + log.innerHTML += '[' + level + '] '; + else + log.innerHTML += '[' + level + '] '; + log.innerHTML += escape(msg) + '\n'; + log.scrollTop = log.scrollHeight; +}; + +rpc.onsubmit = function(ev) { + var text = cmd.value || ''; + var argv = text.trim().split(/\s+/); + var method = argv.shift(); + var params = []; + var i, arg, param; + + cmd.value = ''; + + for (i = 0; i < argv.length; i++) { + arg = argv[i]; + try { + param = JSON.parse(arg); + } catch (e) { + param = arg; + } + params.push(param); + } + + node.rpc.execute({ method: method, params: params }).then(show, show); + + ev.preventDefault(); + ev.stopPropagation(); + + return false; +}; + +send.onsubmit = function(ev) { + var value = document.getElementById('amount').value; + var address = document.getElementById('address').value; + var tx, options; + + options = { + outputs: [{ + address: address, + value: utils.satoshi(value) + }] + }; + + node.wallet.createTX(options).then(function(mtx) { + tx = mtx; + return node.wallet.sign(tx); + }).then(function() { + return node.sendTX(tx); + }).then(function() { + show(tx); + }); + + ev.preventDefault(); + ev.stopPropagation(); + + return false; +}; + +newaddr.onmouseup = function() { + node.wallet.createReceive().then(function() { + formatWallet(node.wallet); + }); +}; + +function kb(size) { + size /= 1000; + return size.toFixed(2) + 'kb'; +} + +function create(html) { + var el = document.createElement('div'); + el.innerHTML = html; + return el.firstChild; +} + +function escape(html, encode) { + return html + .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +function addItem(tx) { + var el; + + if (items.length === 20) { + el = items.shift(); + tdiv.removeChild(el); + el.onmouseup = null; + } + + el = create('' + tx.rhash + ' (' + tx.height + + ' - ' + kb(tx.getSize()) + ')'); + tdiv.appendChild(el); + + setMouseup(el, tx); + + items.push(el); + + chainState.innerHTML = '' + + 'tx=' + node.chain.db.state.tx + + ' coin=' + node.chain.db.state.coin + + ' value=' + utils.btc(node.chain.db.state.value); +} + +function setMouseup(el, obj) { + el.onmouseup = function(ev) { + show(obj); + ev.stopPropagation(); + return false; + }; +} + +function formatWallet(wallet) { + var html = ''; + var key = wallet.master.toJSON().key; + var i, tx, el; + + html += 'Wallet
'; + + if (bcoin.network.primary.witness) { + html += 'Current Address (p2wpkh): ' + + wallet.getAddress() + + '
'; + html += 'Current Address (p2wpkh behind p2sh): ' + + wallet.getProgramAddress() + + '
'; + } else { + html += 'Current Address: ' + wallet.getAddress() + '
'; + } + + html += 'Extended Private Key: ' + key.xprivkey + '
'; + html += 'Mnemonic: ' + key.mnemonic.phrase + '
'; + + wallet.getBalance().then(function(balance) { + html += 'Confirmed Balance: ' + + utils.btc(balance.confirmed) + + '
'; + + html += 'Unconfirmed Balance: ' + + utils.btc(balance.unconfirmed) + + '
'; + + html += 'Balance: ' + utils.btc(balance.total) + '
'; + + return wallet.getHistory(); + }).then(function(txs) { + return wallet.toDetails(txs); + }).then(function(txs) { + html += 'TXs:\n'; + wdiv.innerHTML = html; + + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + + el = create( + '' + + tx.hash + ''); + + wdiv.appendChild(el); + setMouseup(el, tx.toJSON()); + } + }); +} + +options = bcoin.config({ + query: true, + network: 'segnet4', + db: 'leveldb', + useWorkers: true, + coinCache: true, + logger: logger +}); + +bcoin.set(options); + +node = new bcoin.fullnode(options); +node.rpc = new bcoin.rpc(node); + +node.on('error', function(err) { + ; +}); + +node.chain.on('block', addItem); +node.mempool.on('tx', addItem); + +node.open().then(function() { + node.startSync(); + + formatWallet(node.wallet); + + node.wallet.on('balance', function() { + formatWallet(node.wallet); + }); +}); + +})(); diff --git a/browser/server.js b/browser/server.js index dc8d0817..ab271791 100644 --- a/browser/server.js +++ b/browser/server.js @@ -15,22 +15,27 @@ proxy.on('error', function(err) { }); var index = fs.readFileSync(__dirname + '/index.html'); +var indexjs = fs.readFileSync(__dirname + '/index.js'); var bcoin = fs.readFileSync(__dirname + '/bcoin.js'); var worker = fs.readFileSync(__dirname + '/../lib/workers/worker.js'); -server.get('/favicon.ico', function(req, res, next, send) { +server.get('/favicon.ico', function(req, res, send, next) { send(404, '', 'text'); }); -server.get('/', function(req, res, next, send) { +server.get('/', function(req, res, send, next) { send(200, index, 'html'); }); -server.get('/bcoin.js', function(req, res, next, send) { +server.get('/index.js', function(req, res, send, next) { + send(200, indexjs, 'js'); +}); + +server.get('/bcoin.js', function(req, res, send, next) { send(200, bcoin, 'js'); }); -server.get('/bcoin-worker.js', function(req, res, next, send) { +server.get('/bcoin-worker.js', function(req, res, send, next) { send(200, worker, 'js'); }); diff --git a/browser/transform.js b/browser/transform.js new file mode 100644 index 00000000..fcc4266a --- /dev/null +++ b/browser/transform.js @@ -0,0 +1,75 @@ +var Transform = require('stream').Transform; +var path = require('path'); +var assert = require('assert'); +var fs = require('fs'); +var StringDecoder = require('string_decoder').StringDecoder; + +function nil() { + var stream = new Transform(); + + stream._transform = function(chunk, encoding, callback) { + callback(null, chunk); + }; + + stream._flush = function(callback) { + callback(); + }; + + return stream; +} + +function processEnv(str) { + return str.replace( + /^( *)this\.require\('(\w+)', '([^']+)'\)/gm, + '$1this.$2 = require(\'$3\')'); +} + +function processLazy(str) { + str.replace( + /^( *)lazy\('(\w+)', '([^']+)'\)/gm, + function(_, sp, w1, w2) { + str += sp + 'if (0) require(\'' + w2 + '\');\n'; + return ''; + } + ); + return str; +} + +function transformer(file, process) { + var stream = new Transform(); + var decoder = new StringDecoder('utf8'); + var str = ''; + + stream._transform = function(chunk, encoding, callback) { + assert(Buffer.isBuffer(chunk)); + str += decoder.write(chunk); + callback(null, new Buffer(0)); + }; + + stream._flush = function(callback) { + str = process(str); + + stream.push(new Buffer(str, 'utf8')); + + callback(); + }; + + return stream; +} + +function end(file, offset) { + return path.normalize(file).split(path.sep).slice(-offset).join('/'); +} + +module.exports = function(file) { + if (end(file, 3) === 'lib/utils/utils.js') + return transformer(file, processLazy); + + if (end(file, 3) === 'lib/crypto/crypto.js') + return transformer(file, processLazy); + + if (end(file, 2) === 'lib/env.js') + return transformer(file, processEnv); + + return nil(); +}; diff --git a/etc/sample.conf b/etc/sample.conf index e1c3d017..444087e5 100644 --- a/etc/sample.conf +++ b/etc/sample.conf @@ -52,7 +52,6 @@ known-peers: ./known-peers # Miner # payout-address: 1111111111111111111114oLvT2 # coinbase-flags: mined by bcoin -# parallel: false # HTTP # ssl-cert: @/ssl/cert.crt diff --git a/lib/bip70/bip70.js b/lib/bip70/bip70.js deleted file mode 100644 index 3138b09d..00000000 --- a/lib/bip70/bip70.js +++ /dev/null @@ -1,589 +0,0 @@ -/*! - * bip70.js - bip70 for bcoin - * Copyright (c) 2016, Christopher Jeffrey (MIT License). - * https://github.com/bcoin-org/bcoin - */ - -'use strict'; - -var bcoin = require('../env'); -var assert = require('assert'); -var utils = bcoin.utils; -var crypto = require('../crypto/crypto'); -var x509 = require('./x509'); -var asn1 = require('./asn1'); -var protobuf = require('./protobuf'); -var ProtoReader = protobuf.ProtoReader; -var ProtoWriter = protobuf.ProtoWriter; - -function PaymentRequest(options) { - if (!(this instanceof PaymentRequest)) - return new PaymentRequest(options); - - this.version = -1; - this.pkiType = null; - this.pkiData = null; - this.paymentDetails = new PaymentDetails(); - this.signature = null; - - if (options) - this.fromOptions(options); -} - -PaymentRequest.prototype.fromOptions = function fromOptions(options) { - if (options.version != null) { - assert(utils.isNumber(options.version)); - this.version = options.version; - } - - if (options.pkiType != null) { - assert(typeof options.pkiType === 'string'); - this.pkiType = options.pkiType; - } - - if (options.pkiData) { - assert(Buffer.isBuffer(options.pkiData)); - this.pkiData = options.pkiData; - } - - if (options.paymentDetails) - this.paymentDetails.fromOptions(options.paymentDetails); - - if (options.signature) { - assert(Buffer.isBuffer(options.signature)); - this.signature = options.signature; - } - - if (options.chain) - this.setChain(options.chain); - - return this; -}; - -PaymentRequest.fromOptions = function fromOptions(options) { - return new PaymentRequest().fromOptions(options); -}; - -PaymentRequest.prototype.fromRaw = function fromRaw(data) { - var p = new ProtoReader(data); - - this.version = p.readFieldU32(1, true); - this.pkiType = p.readFieldString(2, true); - this.pkiData = p.readFieldBytes(3, true); - this.paymentDetails.fromRaw(p.readFieldBytes(4)); - this.signature = p.readFieldBytes(5, true); - - return this; -}; - -PaymentRequest.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = new Buffer(data, enc); - return new PaymentRequest().fromRaw(data); -}; - -PaymentRequest.prototype.toRaw = function toRaw(writer) { - var p = new ProtoWriter(writer); - - if (this.version !== -1) - p.writeFieldU32(1, this.version); - - if (this.pkiType != null) - p.writeFieldString(2, this.pkiType); - - if (this.pkiData) - p.writeFieldBytes(3, this.pkiData); - - p.writeFieldBytes(4, this.paymentDetails.toRaw()); - - if (this.signature) - p.writeFieldBytes(5, this.signature); - - if (!writer) - p = p.render(); - - return p; -}; - -PaymentRequest.prototype.getAlgorithm = function getAlgorithm() { - var parts; - - if (!this.pkiType) - return; - - parts = this.pkiType.split('+'); - - if (parts.length !== 2) - return; - - if (parts[0] !== 'x509') - return; - - if (parts[1] !== 'sha1' && parts[1] !== 'sha256') - return; - - return { key: parts[0], hash: parts[1] }; -}; - -PaymentRequest.prototype.signatureData = function signatureData() { - var signature = this.signature; - var data; - - this.signature = new Buffer(0); - - data = this.toRaw(); - - this.signature = signature; - - return data; -}; - -PaymentRequest.prototype.signatureHash = function signatureHash() { - var alg = this.getAlgorithm(); - assert(alg, 'No hash algorithm available.'); - return crypto.hash(alg.hash, this.signatureData()); -}; - -PaymentRequest.prototype.setChain = function setChain(chain) { - var p = new ProtoWriter(); - var i, cert, pem; - - if (!Array.isArray(chain)) - chain = [chain]; - - for (i = 0; i < chain.length; i++) { - cert = chain[i]; - if (typeof cert === 'string') { - pem = asn1.fromPEM(cert); - assert(pem.type === 'certificate', 'Bad certificate PEM.'); - cert = pem.data; - } - assert(Buffer.isBuffer(cert), 'Certificates must be PEM or DER.'); - p.writeFieldBytes(1, cert); - } - - this.pkiData = p.render(); -}; - -PaymentRequest.prototype.getChain = function getChain() { - var chain = []; - var p; - - if (!this.pkiData) - return chain; - - p = new ProtoReader(this.pkiData); - - while (p.nextTag() === 1) - chain.push(p.readFieldBytes(1)); - - return chain; -}; - -PaymentRequest.prototype.sign = function sign(key, chain) { - var alg, msg; - - if (chain) - this.setChain(chain); - - if (!this.pkiType) - this.pkiType = 'x509+sha256'; - - alg = this.getAlgorithm(); - assert(alg, 'No hash algorithm available.'); - - msg = this.signatureData(); - chain = this.getChain(); - - this.signature = x509.signSubject(alg.hash, msg, key, chain); -}; - -PaymentRequest.prototype.verify = function verify() { - var alg, msg, sig, chain; - - if (!this.pkiType || this.pkiType === 'none') - return true; - - if (!this.signature) - return false; - - alg = this.getAlgorithm(); - - if (!alg) - return false; - - msg = this.signatureData(); - sig = this.signature; - chain = this.getChain(); - - return x509.verifySubject(alg.hash, msg, sig, chain); -}; - -PaymentRequest.prototype.verifyChain = function verifyChain() { - if (!this.pkiType || this.pkiType === 'none') - return true; - - return x509.verifyChain(this.getChain()); -}; - -PaymentRequest.prototype.getCA = function getCA() { - var chain, root; - - if (!this.pkiType || this.pkiType === 'none') - return; - - chain = this.getChain(); - - if (chain.length === 0) - return; - - root = x509.parse(chain[chain.length - 1]); - - if (!root) - return; - - return { - name: x509.getCAName(root), - trusted: x509.isTrusted(root), - cert: root - }; -}; - -function PaymentDetails(options) { - if (!(this instanceof PaymentDetails)) - return new PaymentDetails(options); - - this.network = null; - this.outputs = []; - this.time = utils.now(); - this.expires = -1; - this.memo = null; - this.paymentUrl = null; - this.merchantData = null; - - if (options) - this.fromOptions(options); -} - -PaymentDetails.prototype.fromOptions = function fromOptions(options) { - var i, output; - - if (options.network != null) { - assert(typeof options.network === 'string'); - this.network = options.network; - } - - if (options.outputs) { - assert(Array.isArray(options.outputs)); - for (i = 0; i < options.outputs.length; i++) { - output = new bcoin.output(options.outputs[i]); - this.outputs.push(output); - } - } - - if (options.time != null) { - assert(utils.isNumber(options.time)); - this.time = options.time; - } - - if (options.expires != null) { - assert(utils.isNumber(options.expires)); - this.expires = options.expires; - } - - if (options.memo != null) { - assert(typeof options.memo === 'string'); - this.memo = options.memo; - } - - if (options.paymentUrl != null) { - assert(typeof options.paymentUrl === 'string'); - this.paymentUrl = options.paymentUrl; - } - - if (options.merchantData) - this.setData(options.merchantData); - - return this; -}; - -PaymentDetails.fromOptions = function fromOptions(options) { - return new PaymentDetails().fromOptions(options); -}; - -PaymentDetails.prototype.isExpired = function isExpired() { - if (this.expires === -1) - return false; - return utils.now() > this.expires; -}; - -PaymentDetails.prototype.setData = function setData(data, enc) { - if (data == null || Buffer.isBuffer(data)) { - this.merchantData = data; - return; - } - - if (typeof data !== 'string') { - assert(!enc || enc === 'json'); - this.merchantData = new Buffer(JSON.stringify(data), 'utf8'); - return; - } - - this.merchantData = new Buffer(data, enc); -}; - -PaymentDetails.prototype.getData = function getData(enc) { - var data = this.merchantData; - - if (!data) - return; - - if (!enc) - return data; - - if (enc === 'json') { - data = data.toString('utf8'); - try { - data = JSON.parse(data); - } catch (e) { - return; - } - return data; - } - - return data.toString(enc); -}; - -PaymentDetails.prototype.fromRaw = function fromRaw(data) { - var p = new ProtoReader(data); - var op, output; - - this.network = p.readFieldString(1, true); - - while (p.nextTag() === 2) { - op = new ProtoReader(p.readFieldBytes(2)); - output = new bcoin.output(); - output.value = op.readFieldU64(1, true); - output.script.fromRaw(op.readFieldBytes(2, true)); - this.outputs.push(output); - } - - this.time = p.readFieldU64(3); - this.expires = p.readFieldU64(4, true); - this.memo = p.readFieldString(5, true); - this.paymentUrl = p.readFieldString(6, true); - this.merchantData = p.readFieldBytes(7, true); - - return this; -}; - -PaymentDetails.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = new Buffer(data, enc); - return new PaymentDetails().fromRaw(data); -}; - -PaymentDetails.prototype.toRaw = function toRaw(writer) { - var p = new ProtoWriter(writer); - var i, op, output; - - if (this.network != null) - p.writeFieldString(1, this.network); - - for (i = 0; i < this.outputs.length; i++) { - output = this.outputs[i]; - op = new ProtoWriter(); - op.writeFieldU64(1, output.value); - op.writeFieldBytes(2, output.script.toRaw()); - p.writeFieldBytes(2, op.render()); - } - - p.writeFieldU64(3, this.time); - - if (this.expires !== -1) - p.writeFieldU64(4, this.expires); - - if (this.memo != null) - p.writeFieldString(5, this.memo); - - if (this.paymentUrl != null) - p.writeFieldString(6, this.paymentUrl); - - if (this.merchantData) - p.writeFieldString(7, this.merchantData); - - if (!writer) - p = p.render(); - - return p; -}; - -function Payment(options) { - if (!(this instanceof Payment)) - return new Payment(options); - - this.merchantData = null; - this.transactions = []; - this.refundTo = []; - this.memo = null; - - if (options) - this.fromOptions(options); -} - -Payment.prototype.fromOptions = function fromOptions(options) { - var i, tx, output; - - if (options.merchantData) - this.setData(options.merchantData); - - if (options.transactions) { - assert(Array.isArray(options.transactions)); - for (i = 0; i < options.transactions.length; i++) { - tx = new bcoin.tx(options.transactions[i]); - this.transactions.push(tx); - } - } - - if (options.refundTo) { - assert(Array.isArray(options.refundTo)); - for (i = 0; i < options.refundTo.length; i++) { - output = new bcoin.output(options.refundTo[i]); - this.refundTo.push(output); - } - } - - if (options.memo != null) { - assert(typeof options.memo === 'string'); - this.memo = options.memo; - } - - return this; -}; - -Payment.fromOptions = function fromOptions(options) { - return new Payment().fromOptions(options); -}; - -Payment.prototype.setData = PaymentDetails.prototype.setData; -Payment.prototype.getData = PaymentDetails.prototype.getData; - -Payment.prototype.fromRaw = function fromRaw(data) { - var p = new ProtoReader(data); - var tx, op, output; - - this.merchantData = p.readFieldBytes(1, true); - - while (p.nextTag() === 2) { - tx = bcoin.tx.fromRaw(p.readFieldBytes(2)); - this.transactions.push(tx); - } - - while (p.nextTag() === 3) { - op = new ProtoReader(p.readFieldBytes(3)); - output = new bcoin.output(); - output.value = op.readFieldU64(1, true); - output.script = bcoin.script.fromRaw(op.readFieldBytes(2, true)); - this.refundTo.push(output); - } - - this.memo = p.readFieldString(4, true); - - return this; -}; - -Payment.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = new Buffer(data, enc); - return new Payment().fromRaw(data); -}; - -Payment.prototype.toRaw = function toRaw(writer) { - var p = new ProtoWriter(writer); - var i, tx, op, output; - - if (this.merchantData) - p.writeFieldBytes(1, this.merchantData); - - for (i = 0; i < this.transactions.length; i++) { - tx = this.transactions[i]; - this.writeFieldBytes(2, tx.toRaw()); - } - - for (i = 0; i < this.refundTo.length; i++) { - op = new ProtoWriter(); - output = this.refundTo[i]; - op.writeFieldU64(1, output.value); - op.writeFieldBytes(2, output.script.toRaw()); - p.writeFieldBytes(3, op.render()); - } - - if (this.memo != null) - p.writeFieldString(4, this.memo); - - if (!writer) - p = p.render(); - - return p; -}; - -function PaymentACK(options) { - if (!(this instanceof PaymentACK)) - return new PaymentACK(options); - - this.payment = new Payment(); - this.memo = null; - - if (options) - this.fromOptions(options); -} - -PaymentACK.prototype.fromOptions = function fromOptions(options) { - if (options.payment) - this.payment.fromOptions(options.payment); - - if (options.memo != null) { - assert(typeof options.memo === 'string'); - this.memo = options.memo; - } - - return this; -}; - -PaymentACK.fromOptions = function fromOptions(options) { - return new PaymentACK().fromOptions(options); -}; - -PaymentACK.prototype.fromRaw = function fromRaw(data) { - var p = new ProtoReader(data); - - this.payment.fromRaw(p.readFieldBytes(1)); - this.memo = p.readFieldString(2, true); - - return this; -}; - -PaymentACK.fromRaw = function fromRaw(data, enc) { - if (typeof data === 'string') - data = new Buffer(data, enc); - return new PaymentACK().fromRaw(data); -}; - -PaymentACK.prototype.toRaw = function toRaw(writer) { - var p = new ProtoWriter(writer); - - p.writeFieldBytes(1, this.payment.toRaw()); - - if (this.memo != null) - p.writeFieldString(2, this.memo); - - if (!writer) - p = p.render(); - - return p; -}; - -exports.PaymentRequest = PaymentRequest; -exports.PaymentDetails = PaymentDetails; -exports.Payment = Payment; -exports.PaymentACK = PaymentACK; diff --git a/lib/bip70/index.js b/lib/bip70/index.js new file mode 100644 index 00000000..a3c90fab --- /dev/null +++ b/lib/bip70/index.js @@ -0,0 +1,15 @@ +/*! + * bip70.js - bip70 for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +exports.PaymentRequest = require('./paymentrequest'); +exports.PaymentDetails = require('./paymentdetails'); +exports.Payment = require('./payment'); +exports.PaymentACK = require('./paymentack'); +exports.asn1 = require('./asn1'); +exports.x509 = require('./x509'); +exports.pk = require('./pk'); diff --git a/lib/bip70/payment.js b/lib/bip70/payment.js new file mode 100644 index 00000000..e539ac1d --- /dev/null +++ b/lib/bip70/payment.js @@ -0,0 +1,127 @@ +/*! + * payment.js - bip70 payment for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var Output = require('../primitives/output'); +var TX = require('../primitives/tx'); +var Script = require('../script/script'); +var protobuf = require('./protobuf'); +var PaymentDetails = require('./paymentdetails'); +var ProtoReader = protobuf.ProtoReader; +var ProtoWriter = protobuf.ProtoWriter; + +function Payment(options) { + if (!(this instanceof Payment)) + return new Payment(options); + + this.merchantData = null; + this.transactions = []; + this.refundTo = []; + this.memo = null; + + if (options) + this.fromOptions(options); +} + +Payment.prototype.fromOptions = function fromOptions(options) { + var i, tx, output; + + if (options.merchantData) + this.setData(options.merchantData); + + if (options.transactions) { + assert(Array.isArray(options.transactions)); + for (i = 0; i < options.transactions.length; i++) { + tx = new TX(options.transactions[i]); + this.transactions.push(tx); + } + } + + if (options.refundTo) { + assert(Array.isArray(options.refundTo)); + for (i = 0; i < options.refundTo.length; i++) { + output = new Output(options.refundTo[i]); + this.refundTo.push(output); + } + } + + if (options.memo != null) { + assert(typeof options.memo === 'string'); + this.memo = options.memo; + } + + return this; +}; + +Payment.fromOptions = function fromOptions(options) { + return new Payment().fromOptions(options); +}; + +Payment.prototype.setData = PaymentDetails.prototype.setData; +Payment.prototype.getData = PaymentDetails.prototype.getData; + +Payment.prototype.fromRaw = function fromRaw(data) { + var p = new ProtoReader(data); + var tx, op, output; + + this.merchantData = p.readFieldBytes(1, true); + + while (p.nextTag() === 2) { + tx = TX.fromRaw(p.readFieldBytes(2)); + this.transactions.push(tx); + } + + while (p.nextTag() === 3) { + op = new ProtoReader(p.readFieldBytes(3)); + output = new Output(); + output.value = op.readFieldU64(1, true); + output.script = Script.fromRaw(op.readFieldBytes(2, true)); + this.refundTo.push(output); + } + + this.memo = p.readFieldString(4, true); + + return this; +}; + +Payment.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new Payment().fromRaw(data); +}; + +Payment.prototype.toRaw = function toRaw(writer) { + var p = new ProtoWriter(writer); + var i, tx, op, output; + + if (this.merchantData) + p.writeFieldBytes(1, this.merchantData); + + for (i = 0; i < this.transactions.length; i++) { + tx = this.transactions[i]; + this.writeFieldBytes(2, tx.toRaw()); + } + + for (i = 0; i < this.refundTo.length; i++) { + op = new ProtoWriter(); + output = this.refundTo[i]; + op.writeFieldU64(1, output.value); + op.writeFieldBytes(2, output.script.toRaw()); + p.writeFieldBytes(3, op.render()); + } + + if (this.memo != null) + p.writeFieldString(4, this.memo); + + if (!writer) + p = p.render(); + + return p; +}; + +module.exports = Payment; diff --git a/lib/bip70/paymentack.js b/lib/bip70/paymentack.js new file mode 100644 index 00000000..37a90bfe --- /dev/null +++ b/lib/bip70/paymentack.js @@ -0,0 +1,71 @@ +/*! + * paymentack.js - bip70 paymentack for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var protobuf = require('./protobuf'); +var Payment = require('./payment'); +var ProtoReader = protobuf.ProtoReader; +var ProtoWriter = protobuf.ProtoWriter; + +function PaymentACK(options) { + if (!(this instanceof PaymentACK)) + return new PaymentACK(options); + + this.payment = new Payment(); + this.memo = null; + + if (options) + this.fromOptions(options); +} + +PaymentACK.prototype.fromOptions = function fromOptions(options) { + if (options.payment) + this.payment.fromOptions(options.payment); + + if (options.memo != null) { + assert(typeof options.memo === 'string'); + this.memo = options.memo; + } + + return this; +}; + +PaymentACK.fromOptions = function fromOptions(options) { + return new PaymentACK().fromOptions(options); +}; + +PaymentACK.prototype.fromRaw = function fromRaw(data) { + var p = new ProtoReader(data); + + this.payment.fromRaw(p.readFieldBytes(1)); + this.memo = p.readFieldString(2, true); + + return this; +}; + +PaymentACK.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new PaymentACK().fromRaw(data); +}; + +PaymentACK.prototype.toRaw = function toRaw(writer) { + var p = new ProtoWriter(writer); + + p.writeFieldBytes(1, this.payment.toRaw()); + + if (this.memo != null) + p.writeFieldString(2, this.memo); + + if (!writer) + p = p.render(); + + return p; +}; + +module.exports = PaymentACK; diff --git a/lib/bip70/paymentdetails.js b/lib/bip70/paymentdetails.js new file mode 100644 index 00000000..45171f82 --- /dev/null +++ b/lib/bip70/paymentdetails.js @@ -0,0 +1,185 @@ +/*! + * paymentdetails.js - bip70 paymentdetails for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var utils = require('../utils/utils'); +var Output = require('../primitives/output'); +var protobuf = require('./protobuf'); +var ProtoReader = protobuf.ProtoReader; +var ProtoWriter = protobuf.ProtoWriter; + +function PaymentDetails(options) { + if (!(this instanceof PaymentDetails)) + return new PaymentDetails(options); + + this.network = null; + this.outputs = []; + this.time = utils.now(); + this.expires = -1; + this.memo = null; + this.paymentUrl = null; + this.merchantData = null; + + if (options) + this.fromOptions(options); +} + +PaymentDetails.prototype.fromOptions = function fromOptions(options) { + var i, output; + + if (options.network != null) { + assert(typeof options.network === 'string'); + this.network = options.network; + } + + if (options.outputs) { + assert(Array.isArray(options.outputs)); + for (i = 0; i < options.outputs.length; i++) { + output = new Output(options.outputs[i]); + this.outputs.push(output); + } + } + + if (options.time != null) { + assert(utils.isNumber(options.time)); + this.time = options.time; + } + + if (options.expires != null) { + assert(utils.isNumber(options.expires)); + this.expires = options.expires; + } + + if (options.memo != null) { + assert(typeof options.memo === 'string'); + this.memo = options.memo; + } + + if (options.paymentUrl != null) { + assert(typeof options.paymentUrl === 'string'); + this.paymentUrl = options.paymentUrl; + } + + if (options.merchantData) + this.setData(options.merchantData); + + return this; +}; + +PaymentDetails.fromOptions = function fromOptions(options) { + return new PaymentDetails().fromOptions(options); +}; + +PaymentDetails.prototype.isExpired = function isExpired() { + if (this.expires === -1) + return false; + return utils.now() > this.expires; +}; + +PaymentDetails.prototype.setData = function setData(data, enc) { + if (data == null || Buffer.isBuffer(data)) { + this.merchantData = data; + return; + } + + if (typeof data !== 'string') { + assert(!enc || enc === 'json'); + this.merchantData = new Buffer(JSON.stringify(data), 'utf8'); + return; + } + + this.merchantData = new Buffer(data, enc); +}; + +PaymentDetails.prototype.getData = function getData(enc) { + var data = this.merchantData; + + if (!data) + return; + + if (!enc) + return data; + + if (enc === 'json') { + data = data.toString('utf8'); + try { + data = JSON.parse(data); + } catch (e) { + return; + } + return data; + } + + return data.toString(enc); +}; + +PaymentDetails.prototype.fromRaw = function fromRaw(data) { + var p = new ProtoReader(data); + var op, output; + + this.network = p.readFieldString(1, true); + + while (p.nextTag() === 2) { + op = new ProtoReader(p.readFieldBytes(2)); + output = new Output(); + output.value = op.readFieldU64(1, true); + output.script.fromRaw(op.readFieldBytes(2, true)); + this.outputs.push(output); + } + + this.time = p.readFieldU64(3); + this.expires = p.readFieldU64(4, true); + this.memo = p.readFieldString(5, true); + this.paymentUrl = p.readFieldString(6, true); + this.merchantData = p.readFieldBytes(7, true); + + return this; +}; + +PaymentDetails.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new PaymentDetails().fromRaw(data); +}; + +PaymentDetails.prototype.toRaw = function toRaw(writer) { + var p = new ProtoWriter(writer); + var i, op, output; + + if (this.network != null) + p.writeFieldString(1, this.network); + + for (i = 0; i < this.outputs.length; i++) { + output = this.outputs[i]; + op = new ProtoWriter(); + op.writeFieldU64(1, output.value); + op.writeFieldBytes(2, output.script.toRaw()); + p.writeFieldBytes(2, op.render()); + } + + p.writeFieldU64(3, this.time); + + if (this.expires !== -1) + p.writeFieldU64(4, this.expires); + + if (this.memo != null) + p.writeFieldString(5, this.memo); + + if (this.paymentUrl != null) + p.writeFieldString(6, this.paymentUrl); + + if (this.merchantData) + p.writeFieldString(7, this.merchantData); + + if (!writer) + p = p.render(); + + return p; +}; + +module.exports = PaymentDetails; diff --git a/lib/bip70/paymentrequest.js b/lib/bip70/paymentrequest.js new file mode 100644 index 00000000..846c6718 --- /dev/null +++ b/lib/bip70/paymentrequest.js @@ -0,0 +1,252 @@ +/*! + * paymentrequest.js - bip70 paymentrequest for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var utils = require('../utils/utils'); +var crypto = require('../crypto/crypto'); +var x509 = require('./x509'); +var asn1 = require('./asn1'); +var protobuf = require('./protobuf'); +var PaymentDetails = require('./paymentdetails'); +var ProtoReader = protobuf.ProtoReader; +var ProtoWriter = protobuf.ProtoWriter; + +function PaymentRequest(options) { + if (!(this instanceof PaymentRequest)) + return new PaymentRequest(options); + + this.version = -1; + this.pkiType = null; + this.pkiData = null; + this.paymentDetails = new PaymentDetails(); + this.signature = null; + + if (options) + this.fromOptions(options); +} + +PaymentRequest.prototype.fromOptions = function fromOptions(options) { + if (options.version != null) { + assert(utils.isNumber(options.version)); + this.version = options.version; + } + + if (options.pkiType != null) { + assert(typeof options.pkiType === 'string'); + this.pkiType = options.pkiType; + } + + if (options.pkiData) { + assert(Buffer.isBuffer(options.pkiData)); + this.pkiData = options.pkiData; + } + + if (options.paymentDetails) + this.paymentDetails.fromOptions(options.paymentDetails); + + if (options.signature) { + assert(Buffer.isBuffer(options.signature)); + this.signature = options.signature; + } + + if (options.chain) + this.setChain(options.chain); + + return this; +}; + +PaymentRequest.fromOptions = function fromOptions(options) { + return new PaymentRequest().fromOptions(options); +}; + +PaymentRequest.prototype.fromRaw = function fromRaw(data) { + var p = new ProtoReader(data); + + this.version = p.readFieldU32(1, true); + this.pkiType = p.readFieldString(2, true); + this.pkiData = p.readFieldBytes(3, true); + this.paymentDetails.fromRaw(p.readFieldBytes(4)); + this.signature = p.readFieldBytes(5, true); + + return this; +}; + +PaymentRequest.fromRaw = function fromRaw(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return new PaymentRequest().fromRaw(data); +}; + +PaymentRequest.prototype.toRaw = function toRaw(writer) { + var p = new ProtoWriter(writer); + + if (this.version !== -1) + p.writeFieldU32(1, this.version); + + if (this.pkiType != null) + p.writeFieldString(2, this.pkiType); + + if (this.pkiData) + p.writeFieldBytes(3, this.pkiData); + + p.writeFieldBytes(4, this.paymentDetails.toRaw()); + + if (this.signature) + p.writeFieldBytes(5, this.signature); + + if (!writer) + p = p.render(); + + return p; +}; + +PaymentRequest.prototype.getAlgorithm = function getAlgorithm() { + var parts; + + if (!this.pkiType) + return; + + parts = this.pkiType.split('+'); + + if (parts.length !== 2) + return; + + if (parts[0] !== 'x509') + return; + + if (parts[1] !== 'sha1' && parts[1] !== 'sha256') + return; + + return { key: parts[0], hash: parts[1] }; +}; + +PaymentRequest.prototype.signatureData = function signatureData() { + var signature = this.signature; + var data; + + this.signature = new Buffer(0); + + data = this.toRaw(); + + this.signature = signature; + + return data; +}; + +PaymentRequest.prototype.signatureHash = function signatureHash() { + var alg = this.getAlgorithm(); + assert(alg, 'No hash algorithm available.'); + return crypto.hash(alg.hash, this.signatureData()); +}; + +PaymentRequest.prototype.setChain = function setChain(chain) { + var p = new ProtoWriter(); + var i, cert, pem; + + if (!Array.isArray(chain)) + chain = [chain]; + + for (i = 0; i < chain.length; i++) { + cert = chain[i]; + if (typeof cert === 'string') { + pem = asn1.fromPEM(cert); + assert(pem.type === 'certificate', 'Bad certificate PEM.'); + cert = pem.data; + } + assert(Buffer.isBuffer(cert), 'Certificates must be PEM or DER.'); + p.writeFieldBytes(1, cert); + } + + this.pkiData = p.render(); +}; + +PaymentRequest.prototype.getChain = function getChain() { + var chain = []; + var p; + + if (!this.pkiData) + return chain; + + p = new ProtoReader(this.pkiData); + + while (p.nextTag() === 1) + chain.push(p.readFieldBytes(1)); + + return chain; +}; + +PaymentRequest.prototype.sign = function sign(key, chain) { + var alg, msg; + + if (chain) + this.setChain(chain); + + if (!this.pkiType) + this.pkiType = 'x509+sha256'; + + alg = this.getAlgorithm(); + assert(alg, 'No hash algorithm available.'); + + msg = this.signatureData(); + chain = this.getChain(); + + this.signature = x509.signSubject(alg.hash, msg, key, chain); +}; + +PaymentRequest.prototype.verify = function verify() { + var alg, msg, sig, chain; + + if (!this.pkiType || this.pkiType === 'none') + return true; + + if (!this.signature) + return false; + + alg = this.getAlgorithm(); + + if (!alg) + return false; + + msg = this.signatureData(); + sig = this.signature; + chain = this.getChain(); + + return x509.verifySubject(alg.hash, msg, sig, chain); +}; + +PaymentRequest.prototype.verifyChain = function verifyChain() { + if (!this.pkiType || this.pkiType === 'none') + return true; + + return x509.verifyChain(this.getChain()); +}; + +PaymentRequest.prototype.getCA = function getCA() { + var chain, root; + + if (!this.pkiType || this.pkiType === 'none') + return; + + chain = this.getChain(); + + if (chain.length === 0) + return; + + root = x509.parse(chain[chain.length - 1]); + + if (!root) + return; + + return { + name: x509.getCAName(root), + trusted: x509.isTrusted(root), + cert: root + }; +}; + +module.exports = PaymentRequest; diff --git a/lib/bip70/protobuf.js b/lib/bip70/protobuf.js index ce5af7f9..6c334585 100644 --- a/lib/bip70/protobuf.js +++ b/lib/bip70/protobuf.js @@ -7,7 +7,7 @@ 'use strict'; var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); diff --git a/lib/chain/chain.js b/lib/chain/chain.js index 2eee5a96..5fd07c10 100644 --- a/lib/chain/chain.js +++ b/lib/chain/chain.js @@ -7,13 +7,19 @@ 'use strict'; -var bcoin = require('../env'); var AsyncObject = require('../utils/async'); -var constants = bcoin.constants; +var Network = require('../protocol/network'); +var Logger = require('../node/logger'); +var ChainDB = require('./chaindb'); +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); -var assert = utils.assert; -var VerifyError = bcoin.errors.VerifyError; +var Locker = require('../utils/locker'); +var ChainEntry = require('./chainentry'); +var assert = require('assert'); +var VerifyError = require('../utils/errors').VerifyError; var VerifyResult = utils.VerifyResult; +var time = require('../net/timedata'); +var co = require('../utils/co'); /** * Represents a blockchain. @@ -65,13 +71,13 @@ function Chain(options) { this.options = options; - this.network = bcoin.network.get(options.network); - this.logger = options.logger || bcoin.defaultLogger; - this.db = new bcoin.chaindb(this); + this.network = Network.get(options.network); + this.logger = options.logger || Logger.global; + this.db = new ChainDB(this); this.total = 0; this.currentBlock = null; this.orphanLimit = options.orphanLimit || (20 << 20); - this.locker = new bcoin.locker(this, this.add); + this.locker = new Locker(true); this.invalid = {}; this.bestHeight = -1; this.tip = null; @@ -179,11 +185,11 @@ Chain.prototype._init = function _init() { /** * Open the chain, wait for the database to load. * @alias Chain#open - * @param {Function} callback + * @returns {Promise} */ -Chain.prototype._open = function open(callback) { - var self = this; +Chain.prototype._open = co(function* open() { + var tip; this.logger.info('Chain is loading.'); @@ -193,67 +199,44 @@ Chain.prototype._open = function open(callback) { if (this.options.coinCache) this.logger.info('Coin cache is enabled.'); - this.db.open(function(err) { - if (err) - return callback(err); + yield this.db.open(); - self.db.getTip(function(err, tip) { - if (err) - return callback(err); + tip = yield this.db.getTip(); - assert(tip); + assert(tip); - self.tip = tip; - self.height = tip.height; + this.tip = tip; + this.height = tip.height; - self.logger.info('Chain Height: %d', tip.height); + this.logger.info('Chain Height: %d', tip.height); - if (tip.height > self.bestHeight) { - self.bestHeight = tip.height; - self.network.updateHeight(tip.height); - } + if (tip.height > this.bestHeight) { + this.bestHeight = tip.height; + this.network.updateHeight(tip.height); + } - self.logger.memory(); + this.logger.memory(); - self.getDeploymentState(function(err, state) { - if (err) - return callback(err); + this.state = yield this.getDeploymentState(); - self.state = state; + this.logger.memory(); - self.logger.memory(); + this.emit('tip', tip); - self.emit('tip', tip); - - if (!self.synced && self.isFull()) { - self.synced = true; - self.emit('full'); - } - - callback(); - }); - }); - }); -}; + if (!this.synced && this.isFull()) { + this.synced = true; + this.emit('full'); + } +}); /** * Close the chain, wait for the database to close. * @alias Chain#close - * @param {Function} callback + * @returns {Promise} */ -Chain.prototype._close = function close(callback) { - this.db.close(callback); -}; - -/** - * Invoke mutex lock. - * @private - * @returns {Function} unlock - */ - -Chain.prototype._lock = function _lock(func, args, force) { - return this.locker.lock(func, args, force); +Chain.prototype._close = function close() { + return this.db.close(); }; /** @@ -261,32 +244,23 @@ Chain.prototype._lock = function _lock(func, args, force) { * @private * @param {Block|MerkleBlock} block * @param {ChainEntry} entry - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Chain.prototype.verifyContext = function verifyContext(block, prev, callback) { - var self = this; +Chain.prototype.verifyContext = co(function* verifyContext(block, prev) { + var state, view; - this.verify(block, prev, function(err, state) { - if (err) - return callback(err); + state = yield this.verify(block, prev); - self.checkDuplicates(block, prev, function(err) { - if (err) - return callback(err); + yield this.checkDuplicates(block, prev); - self.checkInputs(block, prev, state, function(err, view) { - if (err) - return callback(err); + view = yield this.checkInputs(block, prev, state); - // Expose the state globally. - self.state = state; + // Expose the state globally. + this.state = state; - callback(null, view); - }); - }); - }); -}; + return view; +}); /** * Test whether a block is the genesis block. @@ -305,150 +279,143 @@ Chain.prototype.isGenesis = function isGenesis(block) { * @private * @param {Block|MerkleBlock} block * @param {ChainEntry} entry - * @param {Function} callback - Returns + * @returns {Promise} * [{@link VerifyError}, {@link VerifyFlags}]. */ -Chain.prototype.verify = function verify(block, prev, callback) { - var self = this; +Chain.prototype.verify = co(function* verify(block, prev) { var ret = new VerifyResult(); - var i, height, ts, tx, medianTime, commitmentHash; + var i, height, ts, tx, medianTime, commitmentHash, ancestors, state; if (!block.verify(ret)) { - return callback(new VerifyError(block, + throw new VerifyError(block, 'invalid', ret.reason, - ret.score)); + ret.score); } // Skip the genesis block. Skip all blocks in spv mode. if (this.options.spv || this.isGenesis(block)) - return callback(null, this.state); + return this.state; // Ensure it's not an orphan if (!prev) { - return callback(new VerifyError(block, + throw new VerifyError(block, 'invalid', 'bad-prevblk', - 0)); + 0); } if (prev.isHistorical()) - return callback(null, this.state); + return this.state; - prev.getRetargetAncestors(function(err, ancestors) { - if (err) - return callback(err); + ancestors = yield prev.getRetargetAncestors(); - height = prev.height + 1; - medianTime = prev.getMedianTime(ancestors); + height = prev.height + 1; + medianTime = prev.getMedianTime(ancestors); - // Ensure the timestamp is correct - if (block.ts <= medianTime) { - return callback(new VerifyError(block, + // Ensure the timestamp is correct + if (block.ts <= medianTime) { + throw new VerifyError(block, + 'invalid', + 'time-too-old', + 0); + } + + if (block.bits !== this.getTarget(block, prev, ancestors)) { + throw new VerifyError(block, + 'invalid', + 'bad-diffbits', + 100); + } + + state = yield this.getDeployments(block, prev, ancestors); + + // Can't verify any further when merkleblock or headers. + if (this.options.spv) + return state; + + // Make sure the height contained in the coinbase is correct. + if (state.hasBIP34()) { + if (block.getCoinbaseHeight() !== height) { + throw new VerifyError(block, 'invalid', - 'time-too-old', - 0)); + 'bad-cb-height', + 100); } + } - if (block.bits !== self.getTarget(block, prev, ancestors)) { - return callback(new VerifyError(block, - 'invalid', - 'bad-diffbits', - 100)); - } - - self.getDeployments(block, prev, ancestors, function(err, state) { - if (err) - return callback(err); - - // Can't verify any further when merkleblock or headers. - if (self.options.spv) - return callback(null, state); - - // Make sure the height contained in the coinbase is correct. - if (state.hasBIP34()) { - if (block.getCoinbaseHeight() !== height) { - return callback(new VerifyError(block, - 'invalid', - 'bad-cb-height', - 100)); - } - } - - // Check the commitment hash for segwit. - if (state.hasWitness()) { - commitmentHash = block.commitmentHash; - if (commitmentHash) { - if (!block.witnessNonce) { - return callback(new VerifyError(block, - 'invalid', - 'bad-witness-merkle-size', - 100)); - } - if (commitmentHash !== block.getCommitmentHash('hex')) { - return callback(new VerifyError(block, - 'invalid', - 'bad-witness-merkle-match', - 100)); - } - } - } - - // Blocks that do not commit to - // witness data cannot contain it. - if (!commitmentHash) { - if (block.hasWitness()) { - return callback(new VerifyError(block, - 'invalid', - 'unexpected-witness', - 100)); - } - } - - // Check block weight (different from block size - // check in non-contextual verification). - if (block.getWeight() > constants.block.MAX_WEIGHT) { - return callback(new VerifyError(block, + // Check the commitment hash for segwit. + if (state.hasWitness()) { + commitmentHash = block.commitmentHash; + if (commitmentHash) { + if (!block.witnessNonce) { + throw new VerifyError(block, 'invalid', - 'bad-blk-weight', - 100)); + 'bad-witness-merkle-size', + 100); } - - // Get timestamp for tx.isFinal(). - ts = state.hasMTP() ? medianTime : block.ts; - - // Check all transactions - for (i = 0; i < block.txs.length; i++) { - tx = block.txs[i]; - - // Transactions must be finalized with - // regards to nSequence and nLockTime. - if (!tx.isFinal(height, ts)) { - return callback(new VerifyError(block, - 'invalid', - 'bad-txns-nonfinal', - 10)); - } + if (commitmentHash !== block.getCommitmentHash('hex')) { + throw new VerifyError(block, + 'invalid', + 'bad-witness-merkle-match', + 100); } + } + } - callback(null, state); - }); - }); -}; + // Blocks that do not commit to + // witness data cannot contain it. + if (!commitmentHash) { + if (block.hasWitness()) { + throw new VerifyError(block, + 'invalid', + 'unexpected-witness', + 100); + } + } + + // Check block weight (different from block size + // check in non-contextual verification). + if (block.getWeight() > constants.block.MAX_WEIGHT) { + throw new VerifyError(block, + 'invalid', + 'bad-blk-weight', + 100); + } + + // Get timestamp for tx.isFinal(). + ts = state.hasMTP() ? medianTime : block.ts; + + // Check all transactions + for (i = 0; i < block.txs.length; i++) { + tx = block.txs[i]; + + // Transactions must be finalized with + // regards to nSequence and nLockTime. + if (!tx.isFinal(height, ts)) { + throw new VerifyError(block, + 'invalid', + 'bad-txns-nonfinal', + 10); + } + } + + return state; +}); /** * Check all deployments on a chain, ranging from p2sh to segwit. * @param {Block} block * @param {ChainEntry} prev * @param {ChainEntry[]} ancestors - * @param {Function} callback - Returns + * @returns {Promise} * [{@link VerifyError}, {@link DeploymentState}]. */ -Chain.prototype.getDeployments = function getDeployments(block, prev, ancestors, callback) { - var self = this; +Chain.prototype.getDeployments = co(function* getDeployments(block, prev, ancestors) { var state = new DeploymentState(); + var active; // For some reason bitcoind has p2sh in the // mandatory flags by default, when in reality @@ -467,23 +434,23 @@ Chain.prototype.getDeployments = function getDeployments(block, prev, ancestors, // Only allow version 2 blocks (coinbase height) // once the majority of blocks are using it. if (block.version < 2 && prev.isOutdated(2, ancestors)) - return callback(new VerifyError(block, 'obsolete', 'bad-version', 0)); + throw new VerifyError(block, 'obsolete', 'bad-version', 0); // Only allow version 3 blocks (sig validation) // once the majority of blocks are using it. if (block.version < 3 && prev.isOutdated(3, ancestors)) - return callback(new VerifyError(block, 'obsolete', 'bad-version', 0)); + throw new VerifyError(block, 'obsolete', 'bad-version', 0); // Only allow version 4 blocks (checklocktimeverify) // once the majority of blocks are using it. if (block.version < 4 && prev.isOutdated(4, ancestors)) - return callback(new VerifyError(block, 'obsolete', 'bad-version', 0)); + throw new VerifyError(block, 'obsolete', 'bad-version', 0); // Only allow version 5 blocks (bip141 - segnet3) // once the majority of blocks are using it. if (this.options.witness && this.network.oldWitness) { if (block.version < 5 && prev.isOutdated(5, ancestors)) - return callback(new VerifyError(block, 'obsolete', 'bad-version', 0)); + throw new VerifyError(block, 'obsolete', 'bad-version', 0); } // Make sure the height contained in the coinbase is correct. @@ -516,54 +483,33 @@ Chain.prototype.getDeployments = function getDeployments(block, prev, ancestors, } } - utils.serial([ - function(next) { - // CHECKSEQUENCEVERIFY and median time - // past locktimes are now usable (bip9 & bip113). - self.isActive(prev, 'csv', function(err, active) { - if (err) - return next(err); + // CHECKSEQUENCEVERIFY and median time + // past locktimes are now usable (bip9 & bip113). + active = yield this.isActive(prev, 'csv'); + if (active) { + state.flags |= constants.flags.VERIFY_CHECKSEQUENCEVERIFY; + state.lockFlags |= constants.flags.VERIFY_SEQUENCE; + state.lockFlags |= constants.flags.MEDIAN_TIME_PAST; + if (!this.state.hasCSV()) + this.logger.warning('CSV has been activated.'); + } - if (active) { - state.flags |= constants.flags.VERIFY_CHECKSEQUENCEVERIFY; - state.lockFlags |= constants.flags.VERIFY_SEQUENCE; - state.lockFlags |= constants.flags.MEDIAN_TIME_PAST; - if (!self.state.hasCSV()) - self.logger.warning('CSV has been activated.'); - } - - next(); - }); - }, - function(next) { - if (self.network.oldWitness) - return next(); - - // Segregrated witness is now usable (bip141 - segnet4) - self.isActive(prev, 'witness', function(err, active) { - if (err) - return next(err); - - if (active) { - // BIP147 - // state.flags |= constants.flags.VERIFY_NULLDUMMY; - if (self.options.witness) { - state.flags |= constants.flags.VERIFY_WITNESS; - if (!self.state.hasWitness()) - self.logger.warning('Segwit has been activated.'); - } - } - - next(); - }); + // Segregrated witness is now usable (bip141 - segnet4) + if (!this.network.oldWitness) { + active = yield this.isActive(prev, 'witness'); + if (active) { + // BIP147 + // state.flags |= constants.flags.VERIFY_NULLDUMMY; + if (this.options.witness) { + state.flags |= constants.flags.VERIFY_WITNESS; + if (!this.state.hasWitness()) + this.logger.warning('Segwit has been activated.'); + } } - ], function(err) { - if (err) - return callback(err); + } - callback(null, state); - }); -}; + return state; +}); /** * Determine whether to check block for duplicate txids in blockchain @@ -573,40 +519,38 @@ Chain.prototype.getDeployments = function getDeployments(block, prev, ancestors, * @see https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki * @param {Block|MerkleBlock} block * @param {ChainEntry} prev - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Chain.prototype.checkDuplicates = function checkDuplicates(block, prev, callback) { - var self = this; +Chain.prototype.checkDuplicates = co(function* checkDuplicates(block, prev) { var height = prev.height + 1; + var entry; if (this.options.spv) - return callback(); + return; if (this.isGenesis(block)) - return callback(); + return; if (prev.isHistorical()) - return callback(); + return; if (this.network.block.bip34height === -1 || height <= this.network.block.bip34height) { - return this.findDuplicates(block, prev, callback); + yield this.findDuplicates(block, prev); + return; } - this.db.get(this.network.block.bip34height, function(err, entry) { - if (err) - return callback(err); + // It was no longer possible to create duplicate + // TXs once bip34 went into effect. We can check + // for this to avoid a DB lookup. + entry = yield this.db.get(this.network.block.bip34height); - // It was no longer possible to create duplicate - // TXs once bip34 went into effect. We can check - // for this to avoid a DB lookup. - if (entry && entry.hash === self.network.block.bip34hash) - return callback(); + if (entry && entry.hash === this.network.block.bip34hash) + return; - self.findDuplicates(block, prev, callback); - }); -}; + yield this.findDuplicates(block, prev); +}); /** * Check block for duplicate txids in blockchain @@ -616,35 +560,30 @@ Chain.prototype.checkDuplicates = function checkDuplicates(block, prev, callback * @see https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki * @param {Block|MerkleBlock} block * @param {ChainEntry} prev - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Chain.prototype.findDuplicates = function findDuplicates(block, prev, callback) { - var self = this; +Chain.prototype.findDuplicates = co(function* findDuplicates(block, prev) { var height = prev.height + 1; + var i, tx, result; // Check all transactions - utils.forEachSerial(block.txs, function(tx, next) { - // BIP30 - Ensure there are no duplicate txids - self.db.hasCoins(tx.hash(), function(err, result) { - if (err) - return next(err); + for (i = 0; i < block.txs.length; i++) { + tx = block.txs[i]; + result = yield this.db.hasCoins(tx.hash()); - if (result) { - // Blocks 91842 and 91880 created duplicate - // txids by using the same exact output script - // and extraNonce. - if (constants.bip30[height]) { - if (block.hash('hex') === constants.bip30[height]) - return next(); - } - return next(new VerifyError(block, 'invalid', 'bad-txns-BIP30', 100)); + if (result) { + // Blocks 91842 and 91880 created duplicate + // txids by using the same exact output script + // and extraNonce. + if (constants.bip30[height]) { + if (block.hash('hex') === constants.bip30[height]) + continue; } - - next(); - }); - }, callback); -}; + throw new VerifyError(block, 'invalid', 'bad-txns-BIP30', 100); + } + } +}); /** * Check block transactions for all things pertaining @@ -660,117 +599,107 @@ Chain.prototype.findDuplicates = function findDuplicates(block, prev, callback) * @param {Block} block * @param {ChainEntry} prev * @param {DeploymentState} state - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Chain.prototype.checkInputs = function checkInputs(block, prev, state, callback) { - var self = this; +Chain.prototype.checkInputs = co(function* checkInputs(block, prev, state) { var height = prev.height + 1; var historical = prev.isHistorical(); var sigops = 0; + var jobs = []; var ret = new VerifyResult(); + var i, view, tx, valid; if (this.options.spv) - return callback(); + return; if (this.isGenesis(block)) - return callback(); + return; - this.db.getCoinView(block, function(err, view) { - if (err) - return callback(err); + view = yield this.db.getCoinView(block); - // Check all transactions - utils.forEachSerial(block.txs, function(tx, next) { - // Ensure tx is not double spending an output. - if (!tx.isCoinbase()) { - if (!view.fillCoins(tx)) { - assert(!historical, 'BUG: Spent inputs in historical data!'); - return next(new VerifyError(block, - 'invalid', - 'bad-txns-inputs-missingorspent', - 100)); - } + // Check all transactions + for (i = 0; i < block.txs.length; i++) { + tx = block.txs[i]; + + // Ensure tx is not double spending an output. + if (!tx.isCoinbase()) { + if (!view.fillCoins(tx)) { + assert(!historical, 'BUG: Spent inputs in historical data!'); + throw new VerifyError(block, + 'invalid', + 'bad-txns-inputs-missingorspent', + 100); + } + } + + // Skip everything if we're + // using checkpoints. + if (historical) { + view.addTX(tx); + continue; + } + + // Verify sequence locks. + valid = yield this.checkLocks(prev, tx, state.lockFlags); + + if (!valid) { + throw new VerifyError(block, + 'invalid', + 'bad-txns-nonfinal', + 100); + } + + // Count sigops (legacy + scripthash? + witness?) + sigops += tx.getSigopsWeight(state.flags); + + if (sigops > constants.block.MAX_SIGOPS_WEIGHT) { + throw new VerifyError(block, + 'invalid', + 'bad-blk-sigops', + 100); + } + + // Contextual sanity checks. + if (!tx.isCoinbase()) { + if (!tx.checkInputs(height, ret)) { + throw new VerifyError(block, + 'invalid', + ret.reason, + ret.score); } - // Skip everything if we're - // using checkpoints. - if (historical) { - view.addTX(tx); - return next(); - } + // Push onto verification queue. + jobs.push(tx.verifyAsync(state.flags)); + } - // Verify sequence locks. - self.checkLocks(prev, tx, state.lockFlags, function(err, valid) { - if (err) - return next(err); + // Add new coins. + view.addTX(tx); + } - if (!valid) { - return next(new VerifyError(block, - 'invalid', - 'bad-txns-nonfinal', - 100)); - } + if (historical) + return view; - // Count sigops (legacy + scripthash? + witness?) - sigops += tx.getSigopsWeight(state.flags); + // Verify all txs in parallel. + valid = yield co.every(jobs); - if (sigops > constants.block.MAX_SIGOPS_WEIGHT) { - return next(new VerifyError(block, - 'invalid', - 'bad-blk-sigops', - 100)); - } + if (!valid) { + throw new VerifyError(block, + 'invalid', + 'mandatory-script-verify-flag-failed', + 100); + } - // Contextual sanity checks. - if (!tx.isCoinbase()) { - if (!tx.checkInputs(height, ret)) { - return next(new VerifyError(block, - 'invalid', - ret.reason, - ret.score)); - } - } + // Make sure the miner isn't trying to conjure more coins. + if (block.getClaimed() > block.getReward(this.network)) { + throw new VerifyError(block, + 'invalid', + 'bad-cb-amount', + 100); + } - // Add new coins. - view.addTX(tx); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - if (historical) - return callback(null, view); - - // Verify all txs in parallel. - utils.every(block.txs, function(tx, next) { - tx.verifyAsync(state.flags, next); - }, function(err, verified) { - if (err) - return callback(err); - - if (!verified) { - return callback(new VerifyError(block, - 'invalid', - 'mandatory-script-verify-flag-failed', - 100)); - } - - // Make sure the miner isn't trying to conjure more coins. - if (block.getClaimed() > block.getReward(self.network)) { - return callback(new VerifyError(block, - 'invalid', - 'bad-cb-amount', - 100)); - } - - callback(null, view); - }); - }); - }); -}; + return view; +}); /** * Get the cached height for a hash if present. @@ -791,49 +720,28 @@ Chain.prototype._getCachedHeight = function _getCachedHeight(hash) { * @private * @param {ChainEntry} fork - The current chain. * @param {ChainEntry} longer - The competing chain. - * @param {Function} callback - Returns [{@link Error}, {@link ChainEntry}]. + * @returns {Promise} */ -Chain.prototype.findFork = function findFork(fork, longer, callback) { - (function find() { - if (fork.hash === longer.hash) - return callback(null, fork); - - (function next() { - if (longer.height <= fork.height) - return done(); - - longer.getPrevious(function(err, entry) { - if (err) - return callback(err); - - if (!entry) - return callback(new Error('No previous entry for new tip.')); - - longer = entry; - - next(); - }); - })(); - - function done() { - if (fork.hash === longer.hash) - return callback(null, fork); - - fork.getPrevious(function(err, entry) { - if (err) - return callback(err); - - if (!entry) - return callback(new Error('No previous entry for old tip.')); - - fork = entry; - - find(); - }); +Chain.prototype.findFork = co(function* findFork(fork, longer) { + while (fork.hash !== longer.hash) { + while (longer.height > fork.height) { + longer = yield longer.getPrevious(); + if (!longer) + throw new Error('No previous entry for new tip.'); } - })(); -}; + + if (fork.hash === longer.hash) + return fork; + + fork = yield fork.getPrevious(); + + if (!fork) + throw new Error('No previous entry for old tip.'); + } + + return fork; +}); /** * Reorganize the blockchain (connect and disconnect inputs). @@ -842,127 +750,70 @@ Chain.prototype.findFork = function findFork(fork, longer, callback) { * @private * @param {ChainEntry} entry - The competing chain's tip. * @param {Block|MerkleBlock} block - The being being added. - * @param {Function} callback + * @returns {Promise} */ -Chain.prototype.reorganize = function reorganize(entry, block, callback) { - var self = this; +Chain.prototype.reorganize = co(function* reorganize(entry, block) { var tip = this.tip; + var fork = yield this.findFork(tip, entry); + var disconnect = []; + var connect = []; + var i, e; - this.findFork(tip, entry, function(err, fork) { - if (err) - return callback(err); + assert(fork); - assert(fork); + // Disconnect blocks/txs. + e = tip; + while (e.hash !== fork.hash) { + disconnect.push(e); + e = yield e.getPrevious(); + assert(e); + } - // Disconnect blocks/txs. - function disconnect(callback) { - var entries = []; + for (i = 0; i < disconnect.length; i++) { + e = disconnect[i]; + yield this.disconnect(e); + } - (function collect(entry) { - if (entry.hash === fork.hash) - return finish(); + // Disconnect blocks/txs. + e = entry; + while (e.hash !== fork.hash) { + connect.push(e); + e = yield e.getPrevious(); + assert(e); + } - entries.push(entry); + // We don't want to connect the new tip here. + // That will be done outside in setBestChain. + for (i = connect.length - 1; i >= 1; i--) { + e = connect[i]; + yield this.reconnect(e); + } - entry.getPrevious(function(err, entry) { - if (err) - return callback(err); - - assert(entry); - - collect(entry); - }); - })(tip); - - function finish() { - utils.forEachSerial(entries, function(entry, next) { - self.disconnect(entry, next); - }, callback); - } - } - - // Connect blocks/txs. - function reconnect(callback) { - var entries = []; - - (function collect(entry) { - if (entry.hash === fork.hash) - return finish(); - - entries.push(entry); - - entry.getPrevious(function(err, entry) { - if (err) - return callback(err); - - assert(entry); - - collect(entry); - }); - })(entry); - - function finish() { - entries = entries.slice().reverse(); - - // We don't want to connect the new tip here. - // That will be done outside in setBestChain. - entries.pop(); - - utils.forEachSerial(entries, function(entry, next) { - self.reconnect(entry, next); - }, callback); - } - } - - disconnect(function(err) { - if (err) - return callback(err); - - reconnect(function(err) { - if (err) - return callback(err); - - self.emit('reorganize', block, tip.height, tip.hash); - - callback(); - }); - }); - }); -}; + this.emit('reorganize', block, tip.height, tip.hash); +}); /** * Disconnect an entry from the chain (updates the tip). * @param {ChainEntry} entry - * @param {Function} callback + * @returns {Promise} */ -Chain.prototype.disconnect = function disconnect(entry, callback) { - var self = this; +Chain.prototype.disconnect = co(function* disconnect(entry) { + var block = yield this.db.disconnect(entry); + var prev = yield entry.getPrevious(); - this.db.disconnect(entry, function(err, entry, block) { - if (err) - return callback(err); + assert(prev); - entry.getPrevious(function(err, prev) { - if (err) - return callback(err); + this.tip = prev; + this.height = prev.height; - assert(prev); + this.bestHeight = prev.height; + this.network.updateHeight(prev.height); - self.tip = prev; - self.height = prev.height; - - self.bestHeight = prev.height; - self.network.updateHeight(prev.height); - - self.emit('tip', prev); - self.emit('disconnect', entry, block); - - callback(); - }); - }); -}; + this.emit('tip', prev); + this.emit('disconnect', entry, block); +}); /** * Reconnect an entry to the chain (updates the tip). @@ -970,56 +821,43 @@ Chain.prototype.disconnect = function disconnect(entry, callback) { * (necessary because we cannot validate the inputs * in alternate chains when they come in). * @param {ChainEntry} entry - * @param {Function} callback + * @returns {Promise} */ -Chain.prototype.reconnect = function reconnect(entry, callback) { - var self = this; +Chain.prototype.reconnect = co(function* reconnect(entry) { + var block = yield this.db.getBlock(entry.hash); + var prev, view; - this.db.getBlock(entry.hash, function(err, block) { - if (err) - return callback(err); + if (!block) { + assert(this.options.spv); + block = entry.toHeaders(); + } - if (!block) { - assert(self.options.spv); - block = entry.toHeaders(); + prev = yield entry.getPrevious(); + assert(prev); + + try { + view = yield this.verifyContext(block, prev); + } catch (e) { + if (e.type === 'VerifyError') { + this.invalid[entry.hash] = true; + this.emit('invalid', block, entry.height); } + throw e; + } - entry.getPrevious(function(err, prev) { - if (err) - return callback(err); + yield this.db.reconnect(entry, block, view); - assert(prev); + this.tip = entry; + this.height = entry.height; - self.verifyContext(block, prev, function(err, view) { - if (err) { - if (err.type === 'VerifyError') { - self.invalid[entry.hash] = true; - self.emit('invalid', block, entry.height); - } - return callback(err); - } + this.bestHeight = entry.height; + this.network.updateHeight(entry.height); - self.db.reconnect(entry, block, view, function(err) { - if (err) - return callback(err); - - self.tip = entry; - self.height = entry.height; - - self.bestHeight = entry.height; - self.network.updateHeight(entry.height); - - self.emit('tip', entry); - self.emit('reconnect', entry, block); - self.emit('connect', entry, block); - - callback(); - }); - }); - }); - }); -}; + this.emit('tip', entry); + this.emit('reconnect', entry, block); + this.emit('connect', entry, block); +}); /** * Set the best chain. This is called on every valid block @@ -1030,134 +868,125 @@ Chain.prototype.reconnect = function reconnect(entry, callback) { * @param {ChainEntry} entry * @param {Block|MerkleBlock} block * @param {ChainEntry} prev - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Chain.prototype.setBestChain = function setBestChain(entry, block, prev, callback) { - var self = this; +Chain.prototype.setBestChain = co(function* setBestChain(entry, block, prev) { + var view; - function done(err) { - if (err) - return callback(err); - - // Do "contextual" verification on our block - // now that we're certain its previous - // block is in the chain. - self.verifyContext(block, prev, function(err, view) { - if (err) { - // Couldn't verify block. - // Revert the height. - block.setHeight(-1); - - if (err.type === 'VerifyError') { - self.invalid[entry.hash] = true; - self.emit('invalid', block, entry.height); - } - - return callback(err); - } - - // Save block and connect inputs. - self.db.save(entry, block, view, true, function(err) { - if (err) - return callback(err); - - self.tip = entry; - self.height = entry.height; - - self.emit('tip', entry); - - callback(); - }); - }); - } - - // We don't have a genesis block yet. - if (!this.tip) { - if (entry.hash !== this.network.genesis.hash) { - return utils.asyncify(callback)(new VerifyError(block, - 'invalid', - 'bad-genblk', - 100)); - } - - return done(); - } - - // Everything is in order. - if (entry.prevBlock === this.tip.hash) - return done(); + assert(this.tip); // A higher fork has arrived. // Time to reorganize the chain. - this.logger.warning('WARNING: Reorganizing chain.'); - this.reorganize(entry, block, done); -}; + if (entry.prevBlock !== this.tip.hash) { + this.logger.warning('WARNING: Reorganizing chain.'); + yield this.reorganize(entry, block); + } + + // Otherwise, everything is in order. + // Do "contextual" verification on our block + // now that we're certain its previous + // block is in the chain. + try { + view = yield this.verifyContext(block, prev); + } catch (e) { + // Couldn't verify block. + // Revert the height. + block.setHeight(-1); + + if (e.type === 'VerifyError') { + this.invalid[entry.hash] = true; + this.emit('invalid', block, entry.height); + } + + throw e; + } + + // Save block and connect inputs. + yield this.db.save(entry, block, view); + + this.tip = entry; + this.height = entry.height; + + this.emit('tip', entry); +}); /** * Reset the chain to the desired height. This * is useful for replaying the blockchain download * for SPV. * @param {Number} height - * @param {Function} callback + * @returns {Promise} */ -Chain.prototype.reset = function reset(height, callback, force) { - var self = this; +Chain.prototype.reset = co(function* reset(height) { + var unlock = yield this.locker.lock(); + try { + return yield this._reset(height); + } finally { + unlock(); + } +}); - callback = this._lock(reset, [height, callback], force); +/** + * Reset the chain to the desired height without a lock. + * @private + * @param {Number} height + * @returns {Promise} + */ - if (!callback) - return; +Chain.prototype._reset = co(function* reset(height) { + var result = yield this.db.reset(height); - this.db.reset(height, function(err, result) { - if (err) - return callback(err); + // Reset the orphan map completely. There may + // have been some orphans on a forked chain we + // no longer need. + this.purgeOrphans(); - // Reset the orphan map completely. There may - // have been some orphans on a forked chain we - // no longer need. - self.purgeOrphans(); - - callback(null, result); - }); -}; + return result; +}); /** * Reset the chain to the desired timestamp (within 2 * hours). This is useful for replaying the blockchain * download for SPV. * @param {Number} ts - Timestamp. - * @param {Function} callback + * @returns {Promise} */ -Chain.prototype.resetTime = function resetTime(ts, callback) { - var self = this; +Chain.prototype.resetTime = co(function* resetTime(ts) { + var unlock = yield this.locker.lock(); + try { + return yield this._resetTime(ts); + } finally { + unlock(); + } +}); - callback = this._lock(resetTime, [ts, callback]); +/** + * Reset the chain to the desired timestamp without a lock. + * @private + * @param {Number} ts - Timestamp. + * @returns {Promise} + */ - if (!callback) +Chain.prototype._resetTime = co(function* resetTime(ts) { + var entry = yield this.byTime(ts); + + if (!entry) return; - this.byTime(ts, function(err, entry) { - if (err) - return callback(err); - - if (!entry) - return callback(); - - self.reset(entry.height, callback, true); - }, true); -}; + yield this._reset(entry.height); +}); /** * Wait for the chain to drain (finish processing * all of the blocks in its queue). - * @param {Function} callback + * @returns {Promise} */ -Chain.prototype.onDrain = function onDrain(callback) { - this.locker.onDrain(callback); +Chain.prototype.onDrain = function onDrain() { + return this.locker.onDrain(); }; /** @@ -1174,244 +1003,234 @@ Chain.prototype.isBusy = function isBusy() { /** * Add a block to the chain, perform all necessary verification. * @param {Block|MerkleBlock|MemBlock} block - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Chain.prototype.add = function add(block, callback) { - var self = this; - var ret; +Chain.prototype.add = co(function* add(block) { + var unlock = yield this.locker.lock(block); - assert(this.loaded); + assert(!this.currentBlock); - callback = this._lock(add, [block, callback]); + this.currentBlock = block.hash('hex'); - if (!callback) - return; + try { + return yield this._add(block); + } finally { + this.currentBlock = null; + unlock(); + } +}); - ret = new VerifyResult(); +/** + * Add a block to the chain without a lock. + * @private + * @param {Block|MerkleBlock|MemBlock} block + * @returns {Promise} + */ - (function next(block, initial) { - var hash = block.hash('hex'); - var prevBlock = block.prevBlock; - var height, checkpoint, orphan, entry; +Chain.prototype._add = co(function* add(block) { + var ret = new VerifyResult(); + var initial = true; + var hash, prevBlock, height, checkpoint; + var orphan, entry, existing, prev; - self.currentBlock = hash; - self._mark(); + while (block) { + hash = block.hash('hex'); + prevBlock = block.prevBlock; + height = -1; - function handleOrphans() { - // No orphan chain. - if (!self.orphan.map[hash]) - return done(); - - // An orphan chain was found, start resolving. - block = self.orphan.map[hash]; - delete self.orphan.bmap[block.hash('hex')]; - delete self.orphan.map[hash]; - self.orphan.count--; - self.orphan.size -= block.getSize(); - - next(block); - } + // Mark the start time. + this.mark(); // Do not revalidate known invalid blocks. - if (self.invalid[hash] || self.invalid[prevBlock]) { - self.emit('invalid', block, block.getCoinbaseHeight()); - self.invalid[hash] = true; - return done(new VerifyError(block, 'duplicate', 'duplicate', 100)); + if (this.invalid[hash] || this.invalid[prevBlock]) { + this.emit('invalid', block, block.getCoinbaseHeight()); + this.invalid[hash] = true; + throw new VerifyError(block, 'duplicate', 'duplicate', 100); } // Do we already have this block? - if (self.hasPending(hash)) { - self.emit('exists', block, block.getCoinbaseHeight()); - return done(new VerifyError(block, 'duplicate', 'duplicate', 0)); + if (this.hasPending(hash)) { + this.emit('exists', block, block.getCoinbaseHeight()); + throw new VerifyError(block, 'duplicate', 'duplicate', 0); } // If the block is already known to be // an orphan, ignore it. - orphan = self.orphan.map[prevBlock]; + orphan = this.orphan.map[prevBlock]; if (orphan) { // The orphan chain forked. if (orphan.hash('hex') !== hash) { - self.emit('fork', block, + this.emit('fork', block, block.getCoinbaseHeight(), orphan.hash('hex')); } - self.emit('orphan', block, block.getCoinbaseHeight()); + this.emit('orphan', block, block.getCoinbaseHeight()); - return done(new VerifyError(block, 'invalid', 'bad-prevblk', 0)); + throw new VerifyError(block, 'invalid', 'bad-prevblk', 0); } // Special case for genesis block. - if (self.isGenesis(block)) - return done(); + if (hash === this.network.genesis.hash) + break; // Validate the block we want to add. // This is only necessary for new // blocks coming in, not the resolving // orphans. if (initial && !block.verify(ret)) { - self.invalid[hash] = true; - self.emit('invalid', block, block.getCoinbaseHeight()); - return done(new VerifyError(block, 'invalid', ret.reason, ret.score)); + this.invalid[hash] = true; + this.emit('invalid', block, block.getCoinbaseHeight()); + throw new VerifyError(block, 'invalid', ret.reason, ret.score); } - self.db.has(hash, function(err, existing) { - if (err) - return done(err); + existing = yield this.db.has(hash); - // Do we already have this block? - if (existing) { - self.emit('exists', block, block.getCoinbaseHeight()); - return done(new VerifyError(block, 'duplicate', 'duplicate', 0)); + // Do we already have this block? + if (existing) { + this.emit('exists', block, block.getCoinbaseHeight()); + throw new VerifyError(block, 'duplicate', 'duplicate', 0); + } + + // Find the previous block height/index. + prev = yield this.db.get(prevBlock); + + if (prev) + height = prev.height + 1; + + if (height > this.bestHeight) { + this.bestHeight = height; + this.network.updateHeight(height); + } + + // If previous block wasn't ever seen, + // add it current to orphans and break. + if (!prev) { + this.orphan.count++; + this.orphan.size += block.getSize(); + this.orphan.map[prevBlock] = block; + this.orphan.bmap[hash] = block; + + // Update the best height based on the coinbase. + // We do this even for orphans (peers will send + // us their highest block during the initial + // getblocks sync, making it an orphan). + if (block.getCoinbaseHeight() > this.bestHeight) { + this.bestHeight = block.getCoinbaseHeight(); + this.network.updateHeight(this.bestHeight); } - // Find the previous block height/index. - self.db.get(prevBlock, function(err, prev) { - if (err) - return done(err); + this.emit('orphan', block, block.getCoinbaseHeight()); - height = !prev ? -1 : prev.height + 1; + throw new VerifyError(block, 'invalid', 'bad-prevblk', 0); + } - if (height > self.bestHeight) { - self.bestHeight = height; - self.network.updateHeight(height); + // Verify the checkpoint. + if (this.options.useCheckpoints) { + checkpoint = this.network.checkpoints[height]; + if (checkpoint) { + // Someone is either trying to fool us, or + // the consensus protocol is broken and + // there was a 20k+ block reorg. + if (hash !== checkpoint) { + this.logger.warning('Checkpoint mismatch!'); + + this.purgeOrphans(); + + this.emit('fork', block, height, checkpoint); + + throw new VerifyError(block, + 'checkpoint', + 'checkpoint mismatch', + 100); } - // If previous block wasn't ever seen, - // add it current to orphans and break. - if (!prev) { - self.orphan.count++; - self.orphan.size += block.getSize(); - self.orphan.map[prevBlock] = block; - self.orphan.bmap[hash] = block; - - // Update the best height based on the coinbase. - // We do this even for orphans (peers will send - // us their highest block during the initial - // getblocks sync, making it an orphan). - if (block.getCoinbaseHeight() > self.bestHeight) { - self.bestHeight = block.getCoinbaseHeight(); - self.network.updateHeight(self.bestHeight); - } - - self.emit('orphan', block, block.getCoinbaseHeight()); - - return done(new VerifyError(block, 'invalid', 'bad-prevblk', 0)); - } - - // Verify the checkpoint. - if (self.options.useCheckpoints) { - checkpoint = self.network.checkpoints[height]; - if (checkpoint) { - // Someone is very likely trying to fool us. - if (hash !== checkpoint) { - self.purgeOrphans(); - - self.emit('fork', block, height, checkpoint); - - return done(new VerifyError(block, - 'checkpoint', - 'checkpoint mismatch', - 100)); - } - - self.emit('checkpoint', block, height); - } - } - - // Explanation: we try to keep as much data - // off the javascript heap as possible. Blocks - // in the future may be 8mb or 20mb, who knows. - // In fullnode-mode we store the blocks in - // "compact" form (the headers plus the raw - // Buffer object) until they're ready to be - // fully validated here. They are deserialized, - // validated, and emitted. Hopefully the deserialized - // blocks get cleaned up by the GC quickly. - if (block.memory) { - try { - block = block.toBlock(); - } catch (e) { - self.logger.error(e); - return done(new VerifyError(block, - 'malformed', - 'error parsing message', - 100)); - } - } - - // Update the block height early - // Some things in verifyContext may - // need access to height on txs. - block.setHeight(height); - - // Create a new chain entry. - entry = bcoin.chainentry.fromBlock(self, block, prev); - - // The block is on a alternate chain if the - // chainwork is less than or equal to - // our tip's. Add the block but do _not_ - // connect the inputs. - if (entry.chainwork.cmp(self.tip.chainwork) <= 0) { - return self.db.save(entry, block, null, false, function(err) { - if (err) - return done(err); - - // Keep track of stats. - self._done(block, entry); - - // Emit our block (and potentially resolved - // orphan) only if it is on the main chain. - self.emit('competitor', block, entry); - - if (!initial) - self.emit('competitor resolved', block, entry); - - handleOrphans(); - }); - } - - // Attempt to add block to the chain index. - self.setBestChain(entry, block, prev, function(err) { - if (err) - return done(err); - - // Keep track of stats. - self._done(block, entry); - - // Emit our block (and potentially resolved - // orphan) only if it is on the main chain. - self.emit('block', block, entry); - self.emit('connect', entry, block); - - if (!initial) - self.emit('resolved', block, entry); - - handleOrphans(); - }); - }); - }); - })(block, true); - - function done(err) { - // Failsafe for large orphan chains. Do not - // allow more than 20mb stored in memory. - if (self.orphan.size > self.orphanLimit) - self.pruneOrphans(); - - utils.nextTick(function() { - if (!self.synced && self.isFull()) { - self.synced = true; - self.emit('full'); + this.emit('checkpoint', block, height); } + } - self.currentBlock = null; + // Explanation: we try to keep as much data + // off the javascript heap as possible. Blocks + // in the future may be 8mb or 20mb, who knows. + // In fullnode-mode we store the blocks in + // "compact" form (the headers plus the raw + // Buffer object) until they're ready to be + // fully validated here. They are deserialized, + // validated, and emitted. Hopefully the deserialized + // blocks get cleaned up by the GC quickly. + if (block.memory) { + try { + block = block.toBlock(); + } catch (e) { + this.logger.error(e); + throw new VerifyError(block, + 'malformed', + 'error parsing message', + 100); + } + } - callback(err); - }); + // Update the block height early + // Some things in verifyContext may + // need access to height on txs. + block.setHeight(height); + + // Create a new chain entry. + entry = ChainEntry.fromBlock(this, block, prev); + + // The block is on a alternate chain if the + // chainwork is less than or equal to + // our tip's. Add the block but do _not_ + // connect the inputs. + if (entry.chainwork.cmp(this.tip.chainwork) <= 0) { + yield this.db.save(entry, block); + + this.emit('competitor', block, entry); + + if (!initial) + this.emit('competitor resolved', block, entry); + } else { + // Attempt to add block to the chain index. + yield this.setBestChain(entry, block, prev); + + // Emit our block (and potentially resolved + // orphan) only if it is on the main chain. + this.emit('block', block, entry); + this.emit('connect', entry, block); + + if (!initial) + this.emit('resolved', block, entry); + } + + // Keep track of stats. + this.finish(block, entry); + + // No orphan chain. + if (!this.orphan.map[hash]) + break; + + // An orphan chain was found, start resolving. + initial = false; + block = this.orphan.map[hash]; + delete this.orphan.bmap[block.hash('hex')]; + delete this.orphan.map[hash]; + this.orphan.count--; + this.orphan.size -= block.getSize(); } -}; + + // Failsafe for large orphan chains. Do not + // allow more than 20mb stored in memory. + if (this.orphan.size > this.orphanLimit) + this.pruneOrphans(); + + yield co.wait(); + + if (!this.synced && this.isFull()) { + this.synced = true; + this.emit('full'); + } +}); /** * Test whether the chain has reached its slow height. @@ -1419,7 +1238,7 @@ Chain.prototype.add = function add(block, callback) { * @returns {Boolean} */ -Chain.prototype._isSlow = function _isSlow() { +Chain.prototype.isSlow = function isSlow() { if (this.options.spv) return false; @@ -1434,7 +1253,7 @@ Chain.prototype._isSlow = function _isSlow() { * @private */ -Chain.prototype._mark = function _mark() { +Chain.prototype.mark = function mark() { this._time = utils.hrtime(); }; @@ -1446,13 +1265,13 @@ Chain.prototype._mark = function _mark() { * @param {ChainEntry} entry */ -Chain.prototype._done = function _done(block, entry) { +Chain.prototype.finish = function finish(block, entry) { var elapsed, time; // Keep track of total blocks handled. this.total += 1; - if (!this._isSlow()) + if (!this.isSlow()) return; // Report memory for debugging. @@ -1539,89 +1358,74 @@ Chain.prototype.pruneOrphans = function pruneOrphans() { /** * Test the chain to see if it has a block, orphan, or pending block. * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Chain.prototype.has = function has(hash, callback) { +Chain.prototype.has = co(function* has(hash) { if (this.hasOrphan(hash)) - return callback(null, true); + return true; if (this.hasPending(hash)) - return callback(null, true); + return true; if (hash === this.currentBlock) - return callback(null, true); + return true; - this.hasBlock(hash, callback); -}; + return yield this.hasBlock(hash); +}); /** * Find a block entry by timestamp. * @param {Number} ts - Timestamp. - * @param {Function} callback - Returns [Error, {@link ChainEntry}]. + * @returns {Promise} - Returns {@link ChainEntry}. */ -Chain.prototype.byTime = function byTime(ts, callback) { - var self = this; +Chain.prototype.byTime = co(function* byTime(ts) { var start = 0; var end = this.height; - var pos, delta; - - function done(err, result) { - if (err) - return callback(err); - - if (result) - return callback(null, result); - - self.db.get(start, callback); - } + var pos, delta, entry; if (ts >= this.tip.ts) - return utils.asyncify(done)(null, this.tip); + return this.tip; // Do a binary search for a block // mined within an hour of the // timestamp. - (function next() { - if (start >= end) - return done(); - + while (start < end) { pos = (start + end) >>> 1; + entry = yield this.db.get(pos); - self.db.get(pos, function(err, entry) { - if (err) - return done(err); + if (!entry) + return; - delta = Math.abs(ts - entry.ts); + delta = Math.abs(ts - entry.ts); - if (delta <= 60 * 60) - return done(null, entry); + if (delta <= 60 * 60) + break; - if (ts < entry.ts) - end = pos - 1; - else - start = pos + 1; + if (ts < entry.ts) + end = pos - 1; + else + start = pos + 1; + } - next(); - }); - })(); -}; + return entry; +}); /** * Test the chain to see if it contains a block. * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Chain.prototype.hasBlock = function hasBlock(hash, callback) { - this.db.has(hash, callback); +Chain.prototype.hasBlock = function hasBlock(hash) { + return this.db.has(hash); }; /** * Test the chain to see if it contains an orphan. * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ Chain.prototype.hasOrphan = function hasOrphan(hash) { @@ -1631,7 +1435,7 @@ Chain.prototype.hasOrphan = function hasOrphan(hash) { /** * Test the chain to see if it contains a pending block in its queue. * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ Chain.prototype.hasPending = function hasPending(hash) { @@ -1641,11 +1445,11 @@ Chain.prototype.hasPending = function hasPending(hash) { /** * Find the corresponding block entry by hash or height. * @param {Hash|Number} hash/height - * @param {Function} callback - Returns [Error, {@link ChainEntry}]. + * @returns {Promise} - Returns {@link ChainEntry}. */ Chain.prototype.getEntry = function getEntry(hash, callback) { - this.db.get(hash, callback); + return this.db.get(hash); }; /** @@ -1676,17 +1480,19 @@ Chain.prototype.isFull = function isFull() { */ Chain.prototype.isInitial = function isInitial() { - if (!this.tip) - return true; - if (this.synced) return false; if (this.height < this.network.checkpoints.lastHeight) return true; - return this.height < this.bestHeight - 24 * 6 - || this.tip.ts < utils.now() - this.network.block.maxTipAge; + if (this.height < this.bestHeight - 24 * 6) + return true; + + if (this.tip.ts < utils.now() - this.network.block.maxTipAge) + return true; + + return false; }; /** @@ -1695,15 +1501,9 @@ Chain.prototype.isInitial = function isInitial() { */ Chain.prototype.getProgress = function getProgress() { - var start, current, end; - - if (!this.tip) - return 0; - - start = this.network.genesis.ts; - current = this.tip.ts - start; - end = utils.now() - start - 40 * 60; - + var start = this.network.genesis.ts; + var current = this.tip.ts - start; + var end = utils.now() - start - 40 * 60; return Math.min(1, current / end); }; @@ -1712,82 +1512,83 @@ Chain.prototype.getProgress = function getProgress() { * @param {(Number|Hash)?} start - Height or hash to treat as the tip. * The current tip will be used if not present. Note that this can be a * non-existent hash, which is useful for headers-first locators. - * @param {Function} callback - Returns [Error, {@link Hash}[]]. + * @returns {Promise} - Returns {@link Hash}[]. */ -Chain.prototype.getLocator = function getLocator(start, callback) { - var self = this; +Chain.prototype.getLocator = co(function* getLocator(start) { + var unlock = yield this.locker.lock(); + try { + return yield this._getLocator(start); + } finally { + unlock(); + } +}); + +/** + * Calculate chain locator without a lock. + * @private + * @param {(Number|Hash)?} start + * @returns {Promise} + */ + +Chain.prototype._getLocator = co(function* getLocator(start) { var hashes = []; var step = 1; - var height; - - callback = this._lock(getLocator, [start, callback]); - - if (!callback) - return; + var height, entry, main, hash; if (start == null) start = this.tip.hash; - this.db.get(start, function(err, entry) { - if (err) - return callback(err); + entry = yield this.db.get(start); - if (!entry) { - // We could simply return `start` here, - // but there is no required "spacing" - // for locator hashes. Pretend this hash - // is our tip. This is useful for - // getheaders. - if (typeof start === 'string') - hashes.push(start); - entry = self.tip; + if (!entry) { + // We could simply return `start` here, + // but there is no required "spacing" + // for locator hashes. Pretend this hash + // is our tip. This is useful for + // getheaders. + if (typeof start === 'string') + hashes.push(start); + entry = this.tip; + } + + height = entry.height; + main = yield entry.isMainChain(); + hash = entry.hash; + + while (hash) { + hashes.push(hash); + + if (height === 0) + break; + + height = Math.max(height - step, 0); + + if (hashes.length > 10) + step *= 2; + + if (height === 0) { + hash = this.network.genesis.hash; + continue; } - height = entry.height; + // If we're on the main chain, we can + // do a fast lookup of the hash. + if (main) { + hash = yield this.db.getHash(height); + continue; + } - entry.isMainChain(function(err, main) { - if (err) - return callback(err); + entry = yield entry.getAncestorByHeight(height); - (function next(err, hash) { - if (err) - return callback(err); + if (!entry) + break; - if (!hash) - return callback(null, hashes); + hash = entry.hash; + } - hashes.push(hash); - - if (height === 0) - return callback(null, hashes); - - height = Math.max(height - step, 0); - - if (hashes.length > 10) - step *= 2; - - if (height === 0) - return next(null, self.network.genesis.hash); - - // If we're on the main chain, we can - // do a fast lookup of the hash. - if (main) - return self.db.getHash(height, next); - - entry.getAncestorByHeight(height, function(err, entry) { - if (err) - return callback(err); - - if (!entry) - return next(); - - next(null, entry.hash); - }); - })(null, entry.hash); - }); - }); -}; + return hashes; +}); /** * Calculate the orphan root of the hash (if it is an orphan). @@ -1812,46 +1613,41 @@ Chain.prototype.getOrphanRoot = function getOrphanRoot(hash) { /** * Calculate the next target based on the chain tip. - * @param {Function} callback - returns [Error, Number] + * @returns {Promise} - returns Number * (target is in compact/mantissa form). */ -Chain.prototype.getCurrentTarget = function getCurrentTarget(callback) { - if (!this.tip) - return callback(null, this.network.pow.bits); - this.getTargetAsync(null, this.tip, callback); -}; +Chain.prototype.getCurrentTarget = co(function* getCurrentTarget() { + return yield this.getTargetAsync(null, this.tip); +}); /** * Calculate the target based on the passed-in chain entry. * @param {ChainEntry} prev - Previous entry. * @param {Block|MerkleBlock|null} - Current block. - * @param {Function} callback - returns [Error, Number] + * @returns {Promise} - returns Number * (target is in compact/mantissa form). */ -Chain.prototype.getTargetAsync = function getTargetAsync(block, prev, callback) { - var self = this; +Chain.prototype.getTargetAsync = co(function* getTargetAsync(block, prev) { + var ancestors; if ((prev.height + 1) % this.network.pow.retargetInterval !== 0) { if (!this.network.pow.difficultyReset) - return utils.asyncify(callback)(null, this.getTarget(block, prev)); + return this.getTarget(block, prev); } - prev.getAncestors(this.network.pow.retargetInterval, function(err, ancestors) { - if (err) - return callback(err); + ancestors = yield prev.getAncestors(this.network.pow.retargetInterval); - callback(null, self.getTarget(block, prev, ancestors)); - }); -}; + return this.getTarget(block, prev, ancestors); +}); /** * Calculate the target synchronously. _Must_ * have ancestors pre-allocated. * @param {Block|MerkleBlock|null} - Current block. * @param {ChainEntry} prev - Previous entry. - * @param {Function} callback - returns [Error, Number] + * @returns {Promise} - returns Number * (target is in compact/mantissa form). */ @@ -1866,7 +1662,7 @@ Chain.prototype.getTarget = function getTarget(block, prev, ancestors) { if ((prev.height + 1) % this.network.pow.retargetInterval !== 0) { if (this.network.pow.difficultyReset) { // Special behavior for testnet: - ts = block ? (block.ts || block) : bcoin.now(); + ts = block ? (block.ts || block) : time.now(); if (ts > prev.ts + this.network.pow.targetSpacing * 2) return this.network.pow.bits; @@ -1924,30 +1720,23 @@ Chain.prototype.retarget = function retarget(prev, first) { /** * Find a locator. Analagous to bitcoind's `FindForkInGlobalIndex()`. * @param {Hash[]} locator - Hashes. - * @param {Function} callback - Returns [Error, {@link Hash}] (the + * @returns {Promise} - Returns {@link Hash} (the * hash of the latest known block). */ -Chain.prototype.findLocator = function findLocator(locator, callback) { - var self = this; +Chain.prototype.findLocator = co(function* findLocator(locator) { + var i, hash, main; - utils.forEachSerial(locator, function(hash, next) { - self.db.isMainChain(hash, function(err, result) { - if (err) - return next(err); + for (i = 0; i < locator.length; i++) { + hash = locator[i]; + main = yield this.db.isMainChain(hash); - if (result) - return callback(null, hash); + if (main) + return hash; + } - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, self.network.genesis.hash); - }); -}; + return this.network.genesis.hash; +}); /** * Check whether a versionbits deployment is active (BIP9: versionbits). @@ -1956,20 +1745,19 @@ Chain.prototype.findLocator = function findLocator(locator, callback) { * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki * @param {ChainEntry} prev - Previous chain entry. * @param {String} id - Deployment id. - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ -Chain.prototype.isActive = function isActive(prev, id, callback) { +Chain.prototype.isActive = co(function* isActive(prev, id) { + var state; + if (prev.isHistorical()) - return callback(null, false); + return false; - this.getState(prev, id, function(err, state) { - if (err) - return callback(err); + state = yield this.getState(prev, id); - callback(null, state === constants.thresholdStates.ACTIVE); - }); -}; + return state === constants.thresholdStates.ACTIVE; +}); /** * Get chain entry state for a deployment (BIP9: versionbits). @@ -1978,220 +1766,166 @@ Chain.prototype.isActive = function isActive(prev, id, callback) { * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki * @param {ChainEntry} prev - Previous chain entry. * @param {String} id - Deployment id. - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ -Chain.prototype.getState = function getState(prev, id, callback) { - var self = this; +Chain.prototype.getState = co(function* getState(prev, id) { var period = this.network.minerWindow; var threshold = this.network.activationThreshold; var deployment = this.network.deployments[id]; var stateCache = this.stateCache[id]; var timeStart, timeTimeout, compute, height; + var i, entry, count, state, block, medianTime; - if (!deployment) - return callback(null, constants.thresholdStates.FAILED); + assert(deployment); timeStart = deployment.startTime; timeTimeout = deployment.timeout; compute = []; if (!prev) - return callback(null, constants.thresholdStates.DEFINED); + return constants.thresholdStates.DEFINED; if (((prev.height + 1) % period) !== 0) { height = prev.height - ((prev.height + 1) % period); - return prev.getAncestorByHeight(height, function(err, ancestor) { - if (err) - return callback(err); + prev = yield prev.getAncestorByHeight(height); - if (ancestor) { - assert(ancestor.height === height); - assert(((ancestor.height + 1) % period) === 0); - } - - self.getState(ancestor, id, callback); - }); + if (prev) { + assert(prev.height === height); + assert(((prev.height + 1) % period) === 0); + } } - function condition(entry) { - var bits = entry.version & constants.versionbits.TOP_MASK; - var topBits = constants.versionbits.TOP_BITS; - var mask = 1 << deployment.bit; - return bits === topBits && (entry.version & mask) !== 0; + entry = prev; + state = constants.thresholdStates.DEFINED; + + while (entry) { + if (stateCache[entry.hash] != null) { + state = stateCache[entry.hash]; + break; + } + + medianTime = yield entry.getMedianTimeAsync(); + + if (medianTime < timeStart) { + state = constants.thresholdStates.DEFINED; + stateCache[entry.hash] = state; + break; + } + + compute.push(entry); + + height = entry.height - period; + + entry = yield entry.getAncestorByHeight(height); } - (function walk(err, entry) { - if (err) - return callback(err); - - if (!entry) - return walkForward(constants.thresholdStates.DEFINED); - - if (stateCache[entry.hash] != null) - return walkForward(stateCache[entry.hash]); - - entry.getMedianTimeAsync(function(err, medianTime) { - if (err) - return walk(err); - - if (medianTime < timeStart) - return walkForward(constants.thresholdStates.DEFINED); - - compute.push(entry); - - height = entry.height - period; - - entry.getAncestorByHeight(height, walk); - }); - })(null, prev); - - function walkForward(state) { - var entry, count, i; - - if (compute.length === 0) - return callback(null, state); - + while (compute.length) { entry = compute.pop(); switch (state) { case constants.thresholdStates.DEFINED: - return entry.getMedianTimeAsync(function(err, medianTime) { - if (err) - return callback(err); + medianTime = yield entry.getMedianTimeAsync(); - if (medianTime >= timeTimeout) { - stateCache[entry.hash] = constants.thresholdStates.FAILED; - return walkForward(constants.thresholdStates.FAILED); - } + if (medianTime >= timeTimeout) { + state = constants.thresholdStates.FAILED; + break; + } - if (medianTime >= timeStart) { - stateCache[entry.hash] = constants.thresholdStates.STARTED; - return walkForward(constants.thresholdStates.STARTED); - } + if (medianTime >= timeStart) { + state = constants.thresholdStates.STARTED; + break; + } - stateCache[entry.hash] = state; - return walkForward(state); - }); + break; case constants.thresholdStates.STARTED: - return entry.getMedianTimeAsync(function(err, medianTime) { - if (err) - return callback(err); + medianTime = yield entry.getMedianTimeAsync(); - if (medianTime >= timeTimeout) { - stateCache[entry.hash] = constants.thresholdStates.FAILED; - return walkForward(constants.thresholdStates.FAILED); + if (medianTime >= timeTimeout) { + state = constants.thresholdStates.FAILED; + break; + } + + block = entry; + count = 0; + + for (i = 0; i < period; i++) { + if (block.hasBit(deployment)) + count++; + + if (count >= threshold) { + state = constants.thresholdStates.LOCKED_IN; + break; } - count = 0; - i = 0; + block = yield block.getPrevious(); + assert(block); + } - (function next(err, entry) { - if (err) - return callback(err); - - if (!entry) - return doneCounting(); - - if (i++ >= period) - return doneCounting(); - - if (condition(entry)) - count++; - - entry.getPrevious(next); - })(null, entry); - - function doneCounting(err) { - if (err) - return callback(err); - - if (count >= threshold) { - stateCache[entry.hash] = constants.thresholdStates.LOCKED_IN; - return walkForward(constants.thresholdStates.LOCKED_IN); - } - - stateCache[entry.hash] = state; - return walkForward(state); - } - }); + break; case constants.thresholdStates.LOCKED_IN: - stateCache[entry.hash] = constants.thresholdStates.ACTIVE; - return walkForward(constants.thresholdStates.ACTIVE); + state = constants.thresholdStates.ACTIVE; + break; case constants.thresholdStates.FAILED: case constants.thresholdStates.ACTIVE: - stateCache[entry.hash] = state; - return walkForward(state); + break; default: assert(false, 'Bad state.'); break; } + + stateCache[entry.hash] = state; } -}; + + return state; +}); /** * Compute the version for a new block (BIP9: versionbits). * @see https://github.com/bitcoin/bips/blob/master/bip-0009.mediawiki * @param {ChainEntry} prev - Previous chain entry (usually the tip). - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ -Chain.prototype.computeBlockVersion = function computeBlockVersion(prev, callback) { - var self = this; +Chain.prototype.computeBlockVersion = co(function* computeBlockVersion(prev) { var keys = Object.keys(this.network.deployments); var version = 0; + var i, id, deployment, state; - utils.forEachSerial(keys, function(id, next) { - var deployment = self.network.deployments[id]; - self.getState(prev, id, function(err, state) { - if (err) - return next(err); + for (i = 0; i < keys.length; i++) { + id = keys[i]; + deployment = this.network.deployments[id]; + state = yield this.getState(prev, id); - if (state === constants.thresholdStates.LOCKED_IN - || state === constants.thresholdStates.STARTED) { - version |= (1 << deployment.bit); - } + if (state === constants.thresholdStates.LOCKED_IN + || state === constants.thresholdStates.STARTED) { + version |= (1 << deployment.bit); + } + } - next(); - }); - }, function(err) { - if (err) - return callback(err); + version |= constants.versionbits.TOP_BITS; + version >>>= 0; - version |= constants.versionbits.TOP_BITS; - version >>>= 0; - - callback(null, version); - }); -}; + return version; +}); /** * Get the current deployment state of the chain. Called on load. * @private - * @param {Function} callback - Returns [Error, {@link DeploymentState}]. + * @returns {Promise} - Returns {@link DeploymentState}. */ -Chain.prototype.getDeploymentState = function getDeploymentState(callback) { - var self = this; +Chain.prototype.getDeploymentState = co(function* getDeploymentState() { + var prev = yield this.tip.getPrevious(); + var ancestors; - if (!this.tip) - return callback(null, this.state); + if (!prev) + return this.state; - this.tip.getPrevious(function(err, prev) { - if (err) - return callback(err); + ancestors = yield prev.getRetargetAncestors(); - if (!prev) - return callback(null, self.state); - - prev.getRetargetAncestors(function(err, ancestors) { - if (err) - return callback(err); - - self.getDeployments(self.tip, prev, ancestors, callback); - }); - }); -}; + return yield this.getDeployments(this.tip, prev, ancestors); +}); /** * Check transaction finality, taking into account MEDIAN_TIME_PAST @@ -2199,40 +1933,35 @@ Chain.prototype.getDeploymentState = function getDeploymentState(callback) { * @param {ChainEntry} prev - Previous chain entry. * @param {TX} tx * @param {LockFlags} flags - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Chain.prototype.checkFinal = function checkFinal(prev, tx, flags, callback) { +Chain.prototype.checkFinal = co(function* checkFinal(prev, tx, flags) { var height = prev.height + 1; - - function check(err, ts) { - if (err) - return callback(err); - - callback(null, tx.isFinal(height, ts)); - } + var ts; // We can skip MTP if the locktime is height. if (tx.locktime < constants.LOCKTIME_THRESHOLD) - return utils.asyncify(check)(null, -1); + return tx.isFinal(height, -1); - if (flags & constants.flags.MEDIAN_TIME_PAST) - return prev.getMedianTimeAsync(check); + if (flags & constants.flags.MEDIAN_TIME_PAST) { + ts = yield prev.getMedianTimeAsync(); + return tx.isFinal(height, ts); + } - utils.asyncify(check)(null, bcoin.now()); -}; + return tx.isFinal(height, time.now()); +}); /** * Get the necessary minimum time and height sequence locks for a transaction. * @param {TX} tx * @param {LockFlags} flags * @param {ChainEntry} prev - * @param {Function} callback - Returns + * @returns {Promise} * [Error, Number(minTime), Number(minHeight)]. */ -Chain.prototype.getLocks = function getLocks(prev, tx, flags, callback) { - var self = this; +Chain.prototype.getLocks = co(function* getLocks(prev, tx, flags) { var mask = constants.sequence.MASK; var granularity = constants.sequence.GRANULARITY; var disableFlag = constants.sequence.DISABLE_FLAG; @@ -2240,92 +1969,76 @@ Chain.prototype.getLocks = function getLocks(prev, tx, flags, callback) { var hasFlag = flags & constants.flags.VERIFY_SEQUENCE; var minHeight = -1; var minTime = -1; - var coinHeight; + var coinHeight, coinTime; + var i, input, entry; if (tx.isCoinbase() || tx.version < 2 || !hasFlag) - return callback(null, minHeight, minTime); + return new LockTimes(minHeight, minTime); + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; - utils.forEachSerial(tx.inputs, function(input, next) { if (input.sequence & disableFlag) - return next(); + continue; - coinHeight = input.coin.height === -1 - ? self.height + 1 - : input.coin.height; + coinHeight = input.coin.height !== -1 + ? input.coin.height + : this.height + 1; if ((input.sequence & typeFlag) === 0) { coinHeight += (input.sequence & mask) - 1; minHeight = Math.max(minHeight, coinHeight); - return next(); + continue; } - prev.getAncestorByHeight(Math.max(coinHeight - 1, 0), function(err, entry) { - if (err) - return next(err); + entry = yield prev.getAncestorByHeight(Math.max(coinHeight - 1, 0)); + assert(entry, 'Database is corrupt.'); - assert(entry, 'Database is corrupt.'); + coinTime = yield entry.getMedianTimeAsync(); + coinTime += ((input.sequence & mask) << granularity) - 1; + minTime = Math.max(minTime, coinTime); + } - entry.getMedianTimeAsync(function(err, coinTime) { - if (err) - return next(err); - - coinTime += ((input.sequence & mask) << granularity) - 1; - minTime = Math.max(minTime, coinTime); - - next(); - }); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, minHeight, minTime); - }); -}; + return new LockTimes(minHeight, minTime); +}); /** * Evaluate sequence locks. * @param {ChainEntry} prev * @param {Number} minHeight * @param {Number} minTime - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Chain.prototype.evalLocks = function evalLocks(prev, minHeight, minTime, callback) { +Chain.prototype.evalLocks = co(function* evalLocks(prev, minHeight, minTime) { + var medianTime; + if (minHeight >= prev.height + 1) - return callback(null, false); + return false; if (minTime === -1) - return callback(null, true); + return true; - prev.getMedianTimeAsync(function(err, medianTime) { - if (err) - return callback(err); + medianTime = yield prev.getMedianTimeAsync(); - if (minTime >= medianTime) - return callback(null, false); + if (minTime >= medianTime) + return false; - callback(null, true); - }); -}; + return true; +}); /** * Verify sequence locks. * @param {TX} tx * @param {LockFlags} flags * @param {ChainEntry} prev - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Chain.prototype.checkLocks = function checkLocks(prev, tx, flags, callback) { - var self = this; - - this.getLocks(prev, tx, flags, function(err, minHeight, minTime) { - if (err) - return callback(err); - - self.evalLocks(prev, minHeight, minTime, callback); - }); -}; +Chain.prototype.checkLocks = co(function* checkLocks(prev, tx, flags) { + var times = yield this.getLocks(prev, tx, flags); + return yield this.evalLocks(prev, times.height, times.time); +}); /** * Represents the deployment state of the chain. @@ -2408,6 +2121,15 @@ DeploymentState.prototype.hasWitness = function hasWitness() { return (this.flags & constants.flags.VERIFY_WITNESS) !== 0; }; +/* + * LockTimes + */ + +function LockTimes(height, time) { + this.height = height; + this.time = time; +} + /* * Expose */ diff --git a/lib/chain/chaindb.js b/lib/chain/chaindb.js index 4c84c2c3..d30119a7 100644 --- a/lib/chain/chaindb.js +++ b/lib/chain/chaindb.js @@ -7,14 +7,23 @@ 'use strict'; -var bcoin = require('../env'); var AsyncObject = require('../utils/async'); -var constants = bcoin.constants; +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); -var assert = utils.assert; -var DUMMY = new Buffer([0]); +var assert = require('assert'); var BufferWriter = require('../utils/writer'); var BufferReader = require('../utils/reader'); +var co = require('../utils/co'); +var CoinView = require('./coinview'); +var Coins = require('./coins'); +var ldb = require('../db/ldb'); +var LRU = require('../utils/lru'); +var Block = require('../primitives/block'); +var Coin = require('../primitives/coin'); +var TX = require('../primitives/tx'); +var Address = require('../primitives/address'); +var ChainEntry = require('./chainentry'); +var DUMMY = new Buffer([0]); /* * Database Layout: @@ -144,7 +153,6 @@ if (utils.isBrowser) * @param {String?} options.location - Database location * @param {String?} options.db - Database backend name * @property {Boolean} prune - * @property {Boolean} loaded * @property {Number} keepBlocks * @emits ChainDB#open * @emits ChainDB#error @@ -161,7 +169,7 @@ function ChainDB(chain) { this.logger = chain.logger; this.network = chain.network; - this.db = bcoin.ldb({ + this.db = ldb({ location: this.options.location, db: this.options.db, maxOpenFiles: this.options.maxFiles, @@ -177,8 +185,6 @@ function ChainDB(chain) { this.pending = null; this.current = null; - this.loaded = false; - // We want at least 1 retarget interval cached // for retargetting, but we need at least two // cached for optimal versionbits state checks. @@ -190,12 +196,12 @@ function ChainDB(chain) { // We want to keep the last 5 blocks of unspents in memory. this.coinWindow = 25 << 20; - this.coinCache = new bcoin.lru.nil(); - this.cacheHash = new bcoin.lru(this.cacheWindow); - this.cacheHeight = new bcoin.lru(this.cacheWindow); + this.coinCache = new LRU.Nil(); + this.cacheHash = new LRU(this.cacheWindow); + this.cacheHeight = new LRU(this.cacheWindow); if (this.options.coinCache) - this.coinCache = new bcoin.lru(this.coinWindow, getSize); + this.coinCache = new LRU(this.coinWindow, getSize); } utils.inherits(ChainDB, AsyncObject); @@ -210,60 +216,50 @@ ChainDB.layout = layout; /** * Open the chain db, wait for the database to load. * @alias ChainDB#open - * @param {Function} callback + * @returns {Promise} */ -ChainDB.prototype._open = function open(callback) { - var self = this; - var genesis, block; +ChainDB.prototype._open = co(function* open() { + var state, block, entry; this.logger.info('Starting chain load.'); - function done(err) { - if (err) - return callback(err); + yield this.db.open(); - self.logger.info('Chain successfully loaded.'); + yield this.db.checkVersion('V', 1); - self.logger.info( - 'Chain State: hash=%s tx=%d coin=%d value=%s.', - self.state.rhash, - self.state.tx, - self.state.coin, - utils.btc(self.state.value)); + state = yield this.db.get(layout.R); - self.db.checkVersion('V', 1, callback); + if (state) { + // Grab the chainstate if we have one. + this.state = ChainState.fromRaw(state); + } else { + // Otherwise write the genesis block. + // (We assume this database is fresh). + block = Block.fromRaw(this.network.genesisBlock, 'hex'); + block.setHeight(0); + entry = ChainEntry.fromBlock(this.chain, block); + yield this.save(entry, block, new CoinView()); } - this.db.open(function(err) { - if (err) - return done(err); + this.logger.info('Chain successfully loaded.'); - self.db.has(layout.e(self.network.genesis.hash), function(err, exists) { - if (err) - return done(err); - - if (exists) - return self.initState(done); - - block = bcoin.block.fromRaw(self.network.genesisBlock, 'hex'); - block.setHeight(0); - - genesis = bcoin.chainentry.fromBlock(self.chain, block); - - self.save(genesis, block, null, true, done); - }); - }); -}; + this.logger.info( + 'Chain State: hash=%s tx=%d coin=%d value=%s.', + this.state.rhash, + this.state.tx, + this.state.coin, + utils.btc(this.state.value)); +}); /** * Close the chain db, wait for the database to close. * @alias ChainDB#close - * @param {Function} callback + * @returns {Promise} */ -ChainDB.prototype._close = function close(callback) { - this.db.close(callback); +ChainDB.prototype._close = function close() { + return this.db.close(); }; /** @@ -325,37 +321,33 @@ ChainDB.prototype.drop = function drop() { /** * Commit current batch. - * @param {Function} callback + * @returns {Promise} */ -ChainDB.prototype.commit = function commit(callback) { - var self = this; - +ChainDB.prototype.commit = co(function* commit() { assert(this.current); assert(this.pending); - this.current.write(function(err) { - if (err) { - self.current = null; - self.pending = null; - return callback(err); - } + try { + yield this.current.write(); + } catch (e) { + this.current = null; + this.pending = null; + throw e; + } - self.current = null; + this.current = null; - // Overwrite the entire state - // with our new best state - // only if it is committed. - // Note that alternate chain - // tips do not commit anything. - if (self.pending.committed) - self.state = self.pending; + // Overwrite the entire state + // with our new best state + // only if it is committed. + // Note that alternate chain + // tips do not commit anything. + if (this.pending.committed) + this.state = this.pending; - self.pending = null; - - callback(); - }); -}; + this.pending = null; +}); /** * Add an entry to the LRU cache. @@ -400,99 +392,81 @@ ChainDB.prototype.getCache = function getCache(hash) { /** * Get the height of a block by hash. * @param {Hash} hash - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ -ChainDB.prototype.getHeight = function getHeight(hash, callback) { - var entry; +ChainDB.prototype.getHeight = co(function* getHeight(hash) { + var entry, height; checkHash(hash); - if (typeof hash === 'number') { - callback = utils.asyncify(callback); - return callback(null, hash); - } + if (typeof hash === 'number') + return hash; - if (hash === constants.NULL_HASH) { - callback = utils.asyncify(callback); - return callback(null, -1); - } + if (hash === constants.NULL_HASH) + return -1; entry = this.cacheHash.get(hash); - if (entry) { - callback = utils.asyncify(callback); - return callback(null, entry.height); - } + if (entry) + return entry.height; - this.db.fetch(layout.h(hash), function(data) { - assert(data.length === 4, 'Database corruption.'); - return data.readUInt32LE(0, true); - }, function(err, height) { - if (err) - return callback(err); + height = yield this.db.get(layout.h(hash)); - if (height == null) - return callback(null, -1); + if (!height) + return -1; - callback(null, height); - }); -}; + return height.readUInt32LE(0, true); +}); /** * Get the hash of a block by height. Note that this * will only return hashes in the main chain. * @param {Number} height - * @param {Function} callback - Returns [Error, {@link Hash}]. + * @returns {Promise} - Returns {@link Hash}. */ -ChainDB.prototype.getHash = function getHash(height, callback) { - var entry; +ChainDB.prototype.getHash = co(function* getHash(height) { + var entry, hash; checkHash(height); - if (typeof height === 'string') { - callback = utils.asyncify(callback); - return callback(null, height); - } + if (typeof height === 'string') + return height; entry = this.cacheHeight.get(height); - if (entry) { - callback = utils.asyncify(callback); - return callback(null, entry.hash); - } + if (entry) + return entry.hash; - this.db.fetch(layout.H(height), function(data) { - assert(data.length === 32, 'Database corruption.'); - return data.toString('hex'); - }, callback); -}; + hash = yield this.db.get(layout.H(height)); + + if (!hash) + return; + + return hash.toString('hex'); +}); /** * Get the current chain height from the tip record. - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ -ChainDB.prototype.getChainHeight = function getChainHeight(callback) { - this.getTip(function(err, entry) { - if (err) - return callback(err); +ChainDB.prototype.getChainHeight = co(function* getChainHeight() { + var entry = yield this.getTip(); + if (!entry) + return -1; - if (!entry) - return callback(null, -1); - - callback(null, entry.height); - }); -}; + return entry.height; +}); /** * Get both hash and height depending on the value passed in. * @param {Hash|Number} block - Can be a has or height. - * @param {Function} callback - Returns [Error, {@link Hash}, Number]. + * @returns {Promise} - Returns {@link Hash}, Number. */ -ChainDB.prototype.getBoth = function getBoth(block, callback) { +ChainDB.prototype.getBoth = co(function* getBoth(block) { var hash, height; checkHash(block); @@ -503,82 +477,70 @@ ChainDB.prototype.getBoth = function getBoth(block, callback) { height = block; if (!hash) { - return this.getHash(height, function(err, hash) { - if (err) - return callback(err); + hash = yield this.getHash(height); - if (hash == null) - height = -1; + if (hash == null) + height = -1; - callback(null, hash, height); - }); + return [hash, height]; } - this.getHeight(hash, function(err, height) { - if (err) - return callback(err); + height = yield this.getHeight(hash); - if (height === -1) - hash = null; + if (height === -1) + hash = null; - callback(null, hash, height); - }); -}; + return [hash, height]; +}); /** * Retrieve a chain entry but do _not_ add it to the LRU cache. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link ChainEntry}]. + * @returns {Promise} - Returns {@link ChainEntry}. */ -ChainDB.prototype.getEntry = function getEntry(hash, callback) { - var self = this; +ChainDB.prototype.getEntry = co(function* getEntry(hash) { var entry; checkHash(hash); - this.getHash(hash, function(err, hash) { - if (err) - return callback(err); + hash = yield this.getHash(hash); - if (!hash) - return callback(); + if (!hash) + return; - entry = self.cacheHash.get(hash); + entry = this.cacheHash.get(hash); - if (entry) - return callback(null, entry); + if (entry) + return entry; - self.db.fetch(layout.e(hash), function(data) { - return bcoin.chainentry.fromRaw(self.chain, data); - }, callback); - }); -}; + entry = yield this.db.get(layout.e(hash)); + + if (!entry) + return; + + return ChainEntry.fromRaw(this.chain, entry); +}); /** * Retrieve a chain entry and add it to the LRU cache. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link ChainEntry}]. + * @returns {Promise} - Returns {@link ChainEntry}. */ -ChainDB.prototype.get = function get(hash, callback) { - var self = this; +ChainDB.prototype.get = co(function* get(hash) { + var entry = yield this.getEntry(hash); - this.getEntry(hash, function(err, entry) { - if (err) - return callback(err); + if (!entry) + return; - if (!entry) - return callback(); + // There's no efficient way to check whether + // this is in the main chain or not, so + // don't add it to the height cache. + this.cacheHash.set(entry.hash, entry); - // There's no efficient way to check whether - // this is in the main chain or not, so - // don't add it to the height cache. - self.cacheHash.set(entry.hash, entry); - - callback(null, entry); - }); -}; + return entry; +}); /** * Save an entry to the database and optionally @@ -587,34 +549,31 @@ ChainDB.prototype.get = function get(hash, callback) { * instead performed in {@link Chain#add}. * @param {ChainEntry} entry * @param {Block} block - * @param {CoinView} view - * @param {Boolean} connect - Whether to connect the - * block's inputs and add it as a tip. - * @param {Function} callback + * @param {CoinView?} view - Will not connect if null. + * @returns {Promise} */ -ChainDB.prototype.save = function save(entry, block, view, connect, callback) { - var self = this; +ChainDB.prototype.save = co(function* save(entry, block, view) { var hash = block.hash(); var height = new Buffer(4); - this.start(); - height.writeUInt32LE(entry.height, 0, true); + this.start(); + this.put(layout.h(hash), height); this.put(layout.e(hash), entry.toRaw()); this.cacheHash.set(entry.hash, entry); - if (!connect) { - return this.saveBlock(block, view, false, function(err) { - if (err) { - self.drop(); - return callback(err); - } - self.commit(callback); - }); + if (!view) { + try { + yield this.saveBlock(block); + } catch (e) { + this.drop(); + throw e; + } + return yield this.commit(); } this.cacheHeight.set(entry.height, entry); @@ -622,41 +581,25 @@ ChainDB.prototype.save = function save(entry, block, view, connect, callback) { this.put(layout.n(entry.prevBlock), hash); this.put(layout.H(entry.height), hash); - this.saveBlock(block, view, true, function(err) { - if (err) { - self.drop(); - return callback(err); - } - self.put(layout.R, self.pending.commit(hash)); - self.commit(callback); - }); -}; + try { + yield this.saveBlock(block, view); + } catch (e) { + this.drop(); + throw e; + } -/** - * Retrieve the chain state. - * @param {Function} callback - Returns [Error, {@link ChainState}]. - */ + this.put(layout.R, this.pending.commit(hash)); -ChainDB.prototype.initState = function initState(callback) { - var self = this; - this.db.fetch(layout.R, function(data) { - return ChainState.fromRaw(data); - }, function(err, state) { - if (err) - return callback(err); - assert(state); - self.state = state; - return callback(null, state); - }); -}; + yield this.commit(); +}); /** * Retrieve the tip entry from the tip record. - * @param {Function} callback - Returns [Error, {@link ChainEntry}]. + * @returns {Promise} - Returns {@link ChainEntry}. */ -ChainDB.prototype.getTip = function getTip(callback) { - this.get(this.state.hash, callback); +ChainDB.prototype.getTip = function getTip() { + return this.get(this.state.hash); }; /** @@ -664,12 +607,11 @@ ChainDB.prototype.getTip = function getTip(callback) { * @param {ChainEntry} entry * @param {Block} block * @param {CoinView} view - * @param {Function} callback - + * @returns {Promise} - * Returns [Error, {@link ChainEntry}, {@link Block}]. */ -ChainDB.prototype.reconnect = function reconnect(entry, block, view, callback) { - var self = this; +ChainDB.prototype.reconnect = co(function* reconnect(entry, block, view) { var hash = block.hash(); this.start(); @@ -680,30 +622,29 @@ ChainDB.prototype.reconnect = function reconnect(entry, block, view, callback) { this.cacheHash.set(entry.hash, entry); this.cacheHeight.set(entry.height, entry); - this.connectBlock(block, view, function(err) { - if (err) { - self.drop(); - return callback(err); - } + try { + yield this.connectBlock(block, view); + } catch (e) { + this.drop(); + throw e; + } - self.put(layout.R, self.pending.commit(hash)); - self.commit(function(err) { - if (err) - return callback(err); - callback(null, entry, block); - }); - }); -}; + this.put(layout.R, this.pending.commit(hash)); + + yield this.commit(); + + return block; +}); /** * Disconnect block from the chain. * @param {ChainEntry} entry - * @param {Function} callback - + * @returns {Promise} - * Returns [Error, {@link ChainEntry}, {@link Block}]. */ -ChainDB.prototype.disconnect = function disconnect(entry, callback) { - var self = this; +ChainDB.prototype.disconnect = co(function* disconnect(entry) { + var block; this.start(); this.del(layout.n(entry.prevBlock)); @@ -713,64 +654,61 @@ ChainDB.prototype.disconnect = function disconnect(entry, callback) { if (this.options.spv) { this.put(layout.R, this.pending.commit(entry.prevBlock)); - return this.commit(function(err) { - if (err) - return callback(err); - callback(null, entry, entry.toHeaders()); - }); + yield this.commit(); + return entry.toHeaders(); } - this.getBlock(entry.hash, function(err, block) { - if (err) { - self.drop(); - return callback(err); - } + try { + block = yield this.getBlock(entry.hash); + } catch (e) { + this.drop(); + throw e; + } - if (!block) { - self.drop(); - return callback(new Error('Block not found.')); - } + if (!block) { + this.drop(); + throw new Error('Block not found.'); + } - self.disconnectBlock(block, function(err) { - if (err) { - self.drop(); - return callback(err); - } + try { + yield this.disconnectBlock(block); + } catch (e) { + this.drop(); + throw e; + } - self.put(layout.R, self.pending.commit(entry.prevBlock)); - self.commit(function(err) { - if (err) - return callback(err); - callback(null, entry, block); - }); - }); - }); -}; + this.put(layout.R, this.pending.commit(entry.prevBlock)); + + yield this.commit(); + + return block; +}); /** * Get the _next_ block hash (does not work by height). * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link Hash}]. + * @returns {Promise} - Returns {@link Hash}. */ -ChainDB.prototype.getNextHash = function getNextHash(hash, callback) { - return this.db.fetch(layout.n(hash), function(data) { - assert(data.length === 32, 'Database corruption.'); - return data.toString('hex'); - }, callback); -}; +ChainDB.prototype.getNextHash = co(function* getNextHash(hash) { + var data = yield this.db.get(layout.n(hash)); + + if (!data) + return; + + return data.toString('hex'); +}); /** * Check to see if a block is on the main chain. * @param {ChainEntry|Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -ChainDB.prototype.isMainChain = function isMainChain(hash, callback) { - var self = this; - var query; +ChainDB.prototype.isMainChain = co(function* isMainChain(hash) { + var query, height, existing; - if (hash instanceof bcoin.chainentry) { + if (hash instanceof ChainEntry) { query = hash.height; hash = hash.hash; } else { @@ -779,166 +717,139 @@ ChainDB.prototype.isMainChain = function isMainChain(hash, callback) { if (hash === this.chain.tip.hash || hash === this.network.genesis.hash) { - return utils.asyncify(callback)(null, true); + return true; } - this.getHeight(query, function(err, height) { - if (err) - return callback(err); + height = yield this.getHeight(query); + existing = yield this.getHash(height); - self.getHash(height, function(err, existing) { - if (err) - return callback(err); + if (!existing) + return false; - if (!existing) - return callback(null, false); - - callback(null, hash === existing); - }); - }); -}; + return hash === existing; +}); /** * Reset the chain to a height or hash. Useful for replaying * the blockchain download for SPV. * @param {Hash|Number} block - hash/height - * @param {Function} callback + * @returns {Promise} */ -ChainDB.prototype.reset = function reset(block, callback) { - var self = this; +ChainDB.prototype.reset = co(function* reset(block) { + var entry = yield this.get(block); + var tip; - this.get(block, function(err, entry) { - if (err) - return callback(err); + if (!entry) + return; - if (!entry) - return callback(); + tip = yield this.getTip(); - self.getTip(function(err, tip) { - if (err) - return callback(err); + while (tip) { + this.start(); - if (!tip) - return callback(); + if (tip.hash === entry.hash) { + this.put(layout.R, this.pending.commit(tip.hash)); + return yield this.commit(); + } - (function next(err, tip) { - if (err) - return callback(err); + this.del(layout.H(tip.height)); + this.del(layout.h(tip.hash)); + this.del(layout.e(tip.hash)); + this.del(layout.n(tip.prevBlock)); - if (!tip) - return callback(); + try { + yield this.removeBlock(tip.hash); + } catch (e) { + this.drop(); + throw e; + } - self.start(); + this.put(layout.R, this.pending.commit(tip.prevBlock)); - if (tip.hash === entry.hash) { - self.put(layout.R, self.pending.commit(tip.hash)); - return self.commit(callback); - } + yield this.commit(); - self.del(layout.H(tip.height)); - self.del(layout.h(tip.hash)); - self.del(layout.e(tip.hash)); - self.del(layout.n(tip.prevBlock)); - - self.removeBlock(tip.hash, function(err) { - if (err) { - self.drop(); - return callback(err); - } - - self.put(layout.R, self.pending.commit(tip.prevBlock)); - - self.commit(function(err) { - if (err) - return next(err); - self.get(tip.prevBlock, next); - }); - }); - })(null, tip); - }); - }); -}; + tip = yield this.get(tip.prevBlock); + } +}); /** * Test whether the chain contains a block in the * main chain or an alternate chain. Alternate chains will only * be tested if the lookup is done by hash. - * @param {Hash|Number} height - Hash or height. - * @param {Function} callback - Returns [Error, Boolean]. + * @param {Hash|Number} block - Hash or height. + * @returns {Promise} - Returns Boolean. */ -ChainDB.prototype.has = function has(height, callback) { - checkHash(height); +ChainDB.prototype.has = co(function* has(block) { + var items, hash; - this.getBoth(height, function(err, hash, height) { - if (err) - return callback(err); - callback(null, hash != null); - }); -}; + checkHash(block); + + items = yield this.getBoth(block); + hash = items[0]; + + return hash != null; +}); /** * Save a block (not an entry) to the * database and potentially connect the inputs. * @param {Block} block * @param {Boolean} connect - Whether to connect the inputs. - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -ChainDB.prototype.saveBlock = function saveBlock(block, view, connect, callback) { +ChainDB.prototype.saveBlock = co(function* saveBlock(block, view) { if (this.options.spv) - return utils.asyncify(callback)(null, block); + return block; this.put(layout.b(block.hash()), block.toRaw()); - if (!connect) - return utils.asyncify(callback)(null, block); + if (!view) + return block; - this.connectBlock(block, view, callback); -}; + yield this.connectBlock(block, view); + + return block; +}); /** * Remove a block (not an entry) to the database. * Disconnect inputs. * @param {Block|Hash} block - {@link Block} or hash. - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -ChainDB.prototype.removeBlock = function removeBlock(hash, callback) { - var self = this; +ChainDB.prototype.removeBlock = co(function* removeBlock(hash) { + var block = yield this.getBlock(hash); - this.getBlock(hash, function(err, block) { - if (err) - return callback(err); + if (!block) + return; - if (!block) - return callback(); + this.del(layout.b(block.hash())); - self.del(layout.b(block.hash())); - - self.disconnectBlock(block, callback); - }); -}; + return yield this.disconnectBlock(block); +}); /** * Connect block inputs. * @param {Block} block - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -ChainDB.prototype.connectBlock = function connectBlock(block, view, callback) { +ChainDB.prototype.connectBlock = co(function* connectBlock(block, view) { var undo = new BufferWriter(); var i, j, tx, input, output, prev; var hashes, address, hash, coins, raw; if (this.options.spv) - return utils.asyncify(callback)(null, block); + return block; // Genesis block's coinbase is unspendable. if (this.chain.isGenesis(block)) { this.pending.connect(block); - return utils.asyncify(callback)(null, block); + return block; } this.pending.connect(block); @@ -1015,250 +926,222 @@ ChainDB.prototype.connectBlock = function connectBlock(block, view, callback) { if (undo.written > 0) this.put(layout.u(block.hash()), undo.render()); - this.pruneBlock(block, function(err) { - if (err) - return callback(err); - callback(null, block); - }); -}; + yield this.pruneBlock(block); + + return block; +}); /** * Disconnect block inputs. * @param {Block|Hash} block - {@link Block} or hash. - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -ChainDB.prototype.disconnectBlock = function disconnectBlock(block, callback) { - var self = this; - var i, j, tx, input, output, prev; +ChainDB.prototype.disconnectBlock = co(function* disconnectBlock(block) { + var i, j, tx, input, output, prev, view; var hashes, address, hash, coins, raw; if (this.options.spv) - return utils.asyncify(callback)(null, block); + return block; - this.getUndoView(block, function(err, view) { - if (err) - return callback(err); + view = yield this.getUndoView(block); - self.pending.disconnect(block); + this.pending.disconnect(block); - for (i = block.txs.length - 1; i >= 0; i--) { - tx = block.txs[i]; - hash = tx.hash('hex'); + for (i = block.txs.length - 1; i >= 0; i--) { + tx = block.txs[i]; + hash = tx.hash('hex'); - if (self.options.indexTX) { - self.del(layout.t(hash)); - if (self.options.indexAddress) { - hashes = tx.getHashes(); - for (j = 0; j < hashes.length; j++) { - address = hashes[j]; - self.del(layout.T(address, hash)); - } + if (this.options.indexTX) { + this.del(layout.t(hash)); + if (this.options.indexAddress) { + hashes = tx.getHashes(); + for (j = 0; j < hashes.length; j++) { + address = hashes[j]; + this.del(layout.T(address, hash)); } } - - if (!tx.isCoinbase()) { - for (j = 0; j < tx.inputs.length; j++) { - input = tx.inputs[j]; - - assert(input.coin); - - if (self.options.indexAddress) { - address = input.getHash(); - if (address) { - prev = input.prevout; - self.put(layout.C(address, prev.hash, prev.index), DUMMY); - } - } - - self.pending.add(input.coin); - } - } - - // Add all of the coins we are about to - // remove. This is to ensure they appear - // in the view array below. - view.addTX(tx); - - for (j = 0; j < tx.outputs.length; j++) { - output = tx.outputs[j]; - - if (output.script.isUnspendable()) - continue; - - if (self.options.indexAddress) { - address = output.getHash(); - if (address) - self.del(layout.C(address, hash, j)); - } - - // Spend added coin. - view.spend(hash, j); - - self.pending.spend(output); - } } - // Commit new coin state. - view = view.toArray(); + if (!tx.isCoinbase()) { + for (j = 0; j < tx.inputs.length; j++) { + input = tx.inputs[j]; - for (i = 0; i < view.length; i++) { - coins = view[i]; - raw = coins.toRaw(); - if (!raw) { - self.del(layout.c(coins.hash)); - self.coinCache.remove(coins.hash); - } else { - self.put(layout.c(coins.hash), raw); - self.coinCache.set(coins.hash, raw); + assert(input.coin); + + if (this.options.indexAddress) { + address = input.getHash(); + if (address) { + prev = input.prevout; + this.put(layout.C(address, prev.hash, prev.index), DUMMY); + } + } + + this.pending.add(input.coin); } } - self.del(layout.u(block.hash())); + // Add all of the coins we are about to + // remove. This is to ensure they appear + // in the view array below. + view.addTX(tx); - callback(null, block); - }); -}; + for (j = 0; j < tx.outputs.length; j++) { + output = tx.outputs[j]; + + if (output.script.isUnspendable()) + continue; + + if (this.options.indexAddress) { + address = output.getHash(); + if (address) + this.del(layout.C(address, hash, j)); + } + + // Spend added coin. + view.spend(hash, j); + + this.pending.spend(output); + } + } + + // Commit new coin state. + view = view.toArray(); + + for (i = 0; i < view.length; i++) { + coins = view[i]; + raw = coins.toRaw(); + if (!raw) { + this.del(layout.c(coins.hash)); + this.coinCache.remove(coins.hash); + } else { + this.put(layout.c(coins.hash), raw); + this.coinCache.set(coins.hash, raw); + } + } + + this.del(layout.u(block.hash())); + + return block; +}); /** * Fill a transaction with coins (only unspents). * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -ChainDB.prototype.fillCoins = function fillCoins(tx, callback) { - var self = this; +ChainDB.prototype.fillCoins = co(function* fillCoins(tx) { + var i, input, prevout, coin; if (tx.isCoinbase()) - return utils.asyncify(callback)(null, tx); + return tx; + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + prevout = input.prevout; - utils.forEachSerial(tx.inputs, function(input, next) { if (input.coin) - return next(); + continue; - self.getCoin(input.prevout.hash, input.prevout.index, function(err, coin) { - if (err) - return callback(err); + coin = yield this.getCoin(prevout.hash, prevout.index); - if (coin) - input.coin = coin; + if (coin) + input.coin = coin; + } - next(); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, tx); - }); -}; + return tx; +}); /** * Fill a transaction with coins (all historical coins). * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -ChainDB.prototype.fillHistory = function fillHistory(tx, callback) { - var self = this; +ChainDB.prototype.fillHistory = co(function* fillHistory(tx) { + var i, input, ptx; if (!this.options.indexTX) - return utils.asyncify(callback)(null, tx); + return tx; if (tx.isCoinbase()) - return utils.asyncify(callback)(null, tx); + return tx; + + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; - utils.forEachSerial(tx.inputs, function(input, next) { if (input.coin) - return next(); + continue; - self.getTX(input.prevout.hash, function(err, tx) { - if (err) - return next(err); + ptx = yield this.getTX(input.prevout.hash); - if (tx) - input.coin = bcoin.coin.fromTX(tx, input.prevout.index); + if (ptx) + input.coin = Coin.fromTX(ptx, input.prevout.index); + } - next(); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, tx); - }); -}; + return tx; +}); /** * Get a coin (unspents only). * @param {Hash} hash * @param {Number} index - * @param {Function} callback - Returns [Error, {@link Coin}]. + * @returns {Promise} - Returns {@link Coin}. */ -ChainDB.prototype.getCoin = function getCoin(hash, index, callback) { - var self = this; +ChainDB.prototype.getCoin = co(function* getCoin(hash, index) { var coins = this.coinCache.get(hash); - if (coins) { - callback = utils.asyncify(callback); + if (coins) + return Coins.parseCoin(coins, hash, index); - try { - coins = bcoin.coins.parseCoin(coins, hash, index); - } catch (e) { - return callback(e); - } + coins = yield this.db.get(layout.c(hash)); - return callback(null, coins); - } + if (!coins) + return; - this.db.fetch(layout.c(hash), function(data) { - self.coinCache.set(hash, data); - return bcoin.coins.parseCoin(data, hash, index); - }, callback); -}; + this.coinCache.set(hash, coins); + + return Coins.parseCoin(coins, hash, index); +}); /** * Get coins (unspents only). * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link Coins}]. + * @returns {Promise} - Returns {@link Coins}. */ -ChainDB.prototype.getCoins = function getCoins(hash, callback) { - var self = this; +ChainDB.prototype.getCoins = co(function* getCoins(hash) { var coins = this.coinCache.get(hash); - if (coins) { - callback = utils.asyncify(callback); + if (coins) + return Coins.fromRaw(coins, hash); - try { - coins = bcoin.coins.fromRaw(coins, hash); - } catch (e) { - return callback(e); - } + coins = yield this.db.get(layout.c(hash)); - return callback(null, coins); - } + if (!coins) + return; - this.db.fetch(layout.c(hash), function(data) { - self.coinCache.set(hash, data); - return bcoin.coins.fromRaw(data, hash); - }, callback); -}; + this.coinCache.set(hash, coins); + + return Coins.fromRaw(coins, hash); +}); /** * Scan the blockchain for transactions containing specified address hashes. * @param {Hash} start - Block hash to start at. * @param {Hash[]} hashes - Address hashes. * @param {Function} iter - Iterator. Accepts ({@link TX}, {@link Function}). - * @param {Function} callback + * @returns {Promise} */ -ChainDB.prototype.scan = function scan(start, filter, iter, callback) { - var self = this; +ChainDB.prototype.scan = co(function* scan(start, filter, iter) { var total = 0; - var i, j, hashes, hash, tx, txs; + var i, j, entry, hashes, hash, tx, txs, block; if (this.options.spv) - return callback(new Error('Cannot scan in spv mode.')); + throw new Error('Cannot scan in spv mode.'); if (start == null) start = this.network.genesis.hash; @@ -1271,395 +1154,360 @@ ChainDB.prototype.scan = function scan(start, filter, iter, callback) { if (Array.isArray(filter)) filter = utils.toMap(filter); - this.getEntry(start, function(err, entry) { - if (err) - return callback(err); + entry = yield this.getEntry(start); - (function next(err, entry) { - if (err) - return callback(err); + while (entry) { + block = yield this.getFullBlock(entry.hash); + total++; - if (!entry) { - self.logger.info('Finished scanning %d blocks.', total); - return callback(); - } + if (!block) + throw new Error('Block not found.'); - total++; + this.logger.info( + 'Scanning block %s (%d).', + entry.rhash, block.height); - self.getFullBlock(entry.hash, function(err, block) { - if (err) - return next(err); + txs = []; - if (!block) - return next(new Error('Block not found.')); + for (i = 0; i < block.txs.length; i++) { + tx = block.txs[i]; + hashes = tx.getHashes('hex'); - self.logger.info( - 'Scanning block %s (%d).', - entry.rhash, block.height); - - txs = []; - - for (i = 0; i < block.txs.length; i++) { - tx = block.txs[i]; - hashes = tx.getHashes('hex'); - - for (j = 0; j < hashes.length; j++) { - hash = hashes[j]; - if (filter[hash]) { - txs.push(tx); - break; - } - } + for (j = 0; j < hashes.length; j++) { + hash = hashes[j]; + if (filter[hash]) { + txs.push(tx); + break; } + } + } - iter(entry, txs, function(err) { - if (err) - return next(err); - entry.getNext(next); - }); - }); - })(null, entry); - }); -}; + yield iter(entry, txs); + entry = yield entry.getNext(); + } + + this.logger.info('Finished scanning %d blocks.', total); +}); /** * Retrieve a transaction (not filled with coins). * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -ChainDB.prototype.getTX = function getTX(hash, callback) { - if (!this.options.indexTX) - return utils.nextTick(callback); +ChainDB.prototype.getTX = co(function* getTX(hash) { + var data; - this.db.fetch(layout.t(hash), function(data) { - return bcoin.tx.fromExtended(data); - }, callback); -}; + if (!this.options.indexTX) + return; + + data = yield this.db.get(layout.t(hash)); + + if (!data) + return; + + return TX.fromExtended(data); +}); /** * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -ChainDB.prototype.hasTX = function hasTX(hash, callback) { +ChainDB.prototype.hasTX = function hasTX(hash) { if (!this.options.indexTX) - return utils.asyncify(callback)(null, false); + return Promise.resolve(null); - this.db.has(layout.t(hash), callback); + return this.db.has(layout.t(hash)); }; /** * Get all coins pertinent to an address. * @param {Address[]} addresses - * @param {Function} callback - Returns [Error, {@link Coin}[]]. + * @returns {Promise} - Returns {@link Coin}[]. */ -ChainDB.prototype.getCoinsByAddress = function getCoinsByAddress(addresses, callback) { - var self = this; +ChainDB.prototype.getCoinsByAddress = co(function* getCoinsByAddress(addresses) { var coins = []; + var i, j, address, hash, keys, key, coin; if (!this.options.indexAddress) - return utils.asyncify(callback)(null, coins); + return coins; if (!Array.isArray(addresses)) addresses = [addresses]; - utils.forEachSerial(addresses, function(address, next) { - var hash = bcoin.address.getHash(address); + for (i = 0; i < addresses.length; i++) { + address = addresses[i]; + hash = Address.getHash(address); if (!hash) - return next(); + continue; - self.db.iterate({ + keys = yield this.db.keys({ gte: layout.C(hash, constants.ZERO_HASH, 0), lte: layout.C(hash, constants.MAX_HASH, 0xffffffff), parse: layout.Cc - }, function(err, keys) { - if (err) - return next(err); - - utils.forEachSerial(keys, function(key, next) { - self.getCoin(key[0], key[1], function(err, coin) { - if (err) - return callback(err); - - if (coin) - coins.push(coin); - - next(); - }); - }, next); }); - }, function(err) { - if (err) - return callback(err); - callback(null, coins); - }); -}; + + for (j = 0; j < keys.length; j++) { + key = keys[j]; + coin = yield this.getCoin(key[0], key[1]); + + if (coin) + coins.push(coin); + } + } + + return coins; +}); /** * Get all entries. - * @param {Function} callback - Returns [Error, {@link ChainEntry}[]]. + * @returns {Promise} - Returns {@link ChainEntry}[]. */ -ChainDB.prototype.getEntries = function getEntries(callback) { +ChainDB.prototype.getEntries = function getEntries() { var self = this; - this.db.iterate({ + return this.db.values({ gte: layout.e(constants.ZERO_HASH), lte: layout.e(constants.MAX_HASH), - keys: false, - values: true, parse: function(key, value) { - return bcoin.chainentry.fromRaw(self.chain, value); + return ChainEntry.fromRaw(self.chain, value); } - }, callback); + }); }; /** * Get all transaction hashes to an address. * @param {Address[]} addresses - * @param {Function} callback - Returns [Error, {@link Hash}[]]. + * @returns {Promise} - Returns {@link Hash}[]. */ -ChainDB.prototype.getHashesByAddress = function getHashesByAddress(addresses, callback) { - var self = this; +ChainDB.prototype.getHashesByAddress = co(function* getHashesByAddress(addresses) { var hashes = {}; + var i, address, hash; if (!this.options.indexTX || !this.options.indexAddress) - return utils.asyncify(callback)(null, []); + return []; - utils.forEachSerial(addresses, function(address, next) { - var hash = bcoin.address.getHash(address); + for (i = 0; i < addresses.length; i++) { + address = addresses[i]; + hash = Address.getHash(address); if (!hash) - return next(); + continue; - self.db.iterate({ + yield this.db.keys({ gte: layout.T(hash, constants.ZERO_HASH), lte: layout.T(hash, constants.MAX_HASH), parse: function(key) { var hash = layout.Tt(key); hashes[hash] = true; } - }, next); - }, function(err) { - if (err) - return callback(err); - callback(null, Object.keys(hashes)); - }); -}; + }); + } + + return Object.keys(hashes); +}); /** * Get all transactions pertinent to an address. * @param {Address[]} addresses - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -ChainDB.prototype.getTXByAddress = function getTXByAddress(addresses, callback) { - var self = this; +ChainDB.prototype.getTXByAddress = co(function* getTXByAddress(addresses) { var txs = []; + var i, hashes, hash, tx; if (!this.options.indexTX || !this.options.indexAddress) - return utils.asyncify(callback)(null, txs); + return txs; if (!Array.isArray(addresses)) addresses = [addresses]; - this.getHashesByAddress(addresses, function(err, hashes) { - if (err) - return callback(err); + hashes = yield this.getHashesByAddress(addresses); - utils.forEachSerial(hashes, function(hash, next) { - self.getTX(hash, function(err, tx) { - if (err) - return next(err); - txs.push(tx); - next(); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, txs); - }); - }); -}; + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + tx = yield this.getTX(hash); + if (tx) + txs.push(tx); + } + + return txs; +}); /** * Get a transaction and fill it with coins (historical). * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -ChainDB.prototype.getFullTX = function getFullTX(hash, callback) { - var self = this; +ChainDB.prototype.getFullTX = co(function* getFullTX(hash) { + var tx; if (!this.options.indexTX) - return utils.nextTick(callback); + return; - this.getTX(hash, function(err, tx) { - if (err) - return callback(err); + tx = yield this.getTX(hash); - if (!tx) - return callback(); + if (!tx) + return; - self.fillHistory(tx, function(err) { - if (err) - return callback(err); + yield this.fillHistory(tx); - callback(null, tx); - }); - }); -}; + return tx; +}); /** * Get a block and fill it with coins (historical). * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -ChainDB.prototype.getFullBlock = function getFullBlock(hash, callback) { - var self = this; +ChainDB.prototype.getFullBlock = co(function* getFullBlock(hash) { + var block = yield this.getBlock(hash); - this.getBlock(hash, function(err, block) { - if (err) - return callback(err); + if (!block) + return; - if (!block) - return callback(); + yield this.getUndoView(block); - self.getUndoView(block, function(err, view) { - if (err) - return callback(err); - - callback(null, block); - }); - }); -}; + return block; +}); /** * Get a view of the existing coins necessary to verify a block. * @param {Block} block - * @param {Function} callback - Returns [Error, {@link CoinView}]. + * @returns {Promise} - Returns {@link CoinView}. */ -ChainDB.prototype.getCoinView = function getCoinView(block, callback) { - var self = this; - var view = new bcoin.coinview(); +ChainDB.prototype.getCoinView = co(function* getCoinView(block, callback) { + var view = new CoinView(); + var prevout = block.getPrevout(); + var i, prev, coins; - utils.forEachSerial(block.getPrevout(), function(prevout, next) { - self.getCoins(prevout, function(err, coins) { - if (err) - return next(err); + for (i = 0; i < prevout.length; i++) { + prev = prevout[i]; + coins = yield this.getCoins(prev); + if (coins) + view.add(coins); + } - if (coins) - view.add(coins); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, view); - }); -}; + return view; +}); /** * Get coins necessary to be resurrected during a reorg. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link Coin}[]]. + * @returns {Promise} - Returns {@link Coin}[]. */ -ChainDB.prototype.getUndoCoins = function getUndoCoins(hash, callback) { - this.db.fetch(layout.u(hash), function(data) { - var p = new BufferReader(data); - var coins = []; +ChainDB.prototype.getUndoCoins = co(function* getUndoCoins(hash) { + var data = yield this.db.get(layout.u(hash)); + var p, coins; - while (p.left()) - coins.push(bcoin.coin.fromRaw(p)); + if (!data) + return; - return coins; - }, callback); -}; + p = new BufferReader(data); + coins = []; + + while (p.left()) + coins.push(Coin.fromRaw(p)); + + return coins; +}); /** * Get a coin view containing unspent coins as * well as the coins to be resurrected for a reorg. * (Note: fills block with undo coins). * @param {Block} block - * @param {Function} callback - Returns [Error, {@link CoinView}]. + * @returns {Promise} - Returns {@link CoinView}. */ -ChainDB.prototype.getUndoView = function getUndoView(block, callback) { - var self = this; - var i, j, k, tx, input, coin; +ChainDB.prototype.getUndoView = co(function* getUndoView(block) { + var i, j, k, tx, input, coin, view, coins; - this.getCoinView(block, function(err, view) { - if (err) - return callback(err); + view = yield this.getCoinView(block); + coins = yield this.getUndoCoins(block.hash()); - self.getUndoCoins(block.hash(), function(err, coins) { - if (err) - return callback(err); + if (!coins) + return view; - if (!coins) - return callback(null, view); + for (i = 0, k = 0; i < block.txs.length; i++) { + tx = block.txs[i]; - for (i = 0, k = 0; i < block.txs.length; i++) { - tx = block.txs[i]; + if (tx.isCoinbase()) + continue; - if (tx.isCoinbase()) - continue; + for (j = 0; j < tx.inputs.length; j++) { + input = tx.inputs[j]; + coin = coins[k++]; + coin.hash = input.prevout.hash; + coin.index = input.prevout.index; + input.coin = coin; + view.addCoin(coin); + } + } - for (j = 0; j < tx.inputs.length; j++) { - input = tx.inputs[j]; - coin = coins[k++]; - coin.hash = input.prevout.hash; - coin.index = input.prevout.index; - input.coin = coin; - view.addCoin(coin); - } - } - - callback(null, view); - }); - }); -}; + return view; +}); /** * Retrieve a block from the database (not filled with coins). * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -ChainDB.prototype.getBlock = function getBlock(hash, callback) { - var self = this; - this.getBoth(hash, function(err, hash, height) { - if (err) - return callback(err); +ChainDB.prototype.getBlock = co(function* getBlock(hash) { + var items = yield this.getBoth(hash); + var height, data, block; - if (!hash) - return callback(); + hash = items[0]; + height = items[1]; - self.db.fetch(layout.b(hash), function(data) { - var block = bcoin.block.fromRaw(data); - block.setHeight(height); - return block; - }, callback); - }); -}; + if (!hash) + return; + + data = yield this.db.get(layout.b(hash)); + + if (!data) + return; + + block = Block.fromRaw(data); + block.setHeight(height); + + return block; +}); + +/** + * Retrieve a block from the database (not filled with coins). + * @param {Hash} hash + * @returns {Promise} - Returns {@link Block}. + */ + +ChainDB.prototype.getRawBlock = co(function* getRawBlock(block) { + var hash = yield this.getHash(block); + + if (!hash) + return; + + return yield this.db.get(layout.b(hash)); +}); /** * Check whether coins are still unspent. Necessary for bip30. * @see https://bitcointalk.org/index.php?topic=67738.0 * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -ChainDB.prototype.hasCoins = function hasCoins(hash, callback) { - this.db.has(layout.c(hash), callback); +ChainDB.prototype.hasCoins = function hasCoins(hash) { + return this.db.has(layout.c(hash)); }; /** @@ -1667,21 +1515,20 @@ ChainDB.prototype.hasCoins = function hasCoins(hash, callback) { * add current block to the prune queue. * @private * @param {Block} - * @param {Function} callback + * @returns {Promise} */ -ChainDB.prototype.pruneBlock = function pruneBlock(block, callback) { - var self = this; - var futureHeight, key; +ChainDB.prototype.pruneBlock = co(function* pruneBlock(block) { + var futureHeight, key, hash; if (this.options.spv) - return callback(); + return; if (!this.prune) - return callback(); + return; if (block.height <= this.network.block.pruneAfterHeight) - return callback(); + return; futureHeight = block.height + this.keepBlocks; @@ -1689,23 +1536,15 @@ ChainDB.prototype.pruneBlock = function pruneBlock(block, callback) { key = layout.q(block.height); - this.db.fetch(key, function(data) { - assert(data.length === 32, 'Database corruption.'); - return data.toString('hex'); - }, function(err, hash) { - if (err) - return callback(err); + hash = yield this.db.get(key); - if (!hash) - return callback(); + if (!hash) + return; - self.del(key); - self.del(layout.b(hash)); - self.del(layout.u(hash)); - - callback(); - }); -}; + this.del(key); + this.del(layout.b(hash)); + this.del(layout.u(hash)); +}); /** * Chain State diff --git a/lib/chain/chainentry.js b/lib/chain/chainentry.js index 12709f5c..455955a2 100644 --- a/lib/chain/chainentry.js +++ b/lib/chain/chainentry.js @@ -7,14 +7,17 @@ 'use strict'; -var bcoin = require('../env'); var bn = require('bn.js'); -var constants = bcoin.constants; +var Network = require('../protocol/network'); +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; +var assert = require('assert'); var BufferWriter = require('../utils/writer'); var BufferReader = require('../utils/reader'); +var Headers = require('../primitives/headers'); +var InvItem = require('../primitives/invitem'); +var co = require('../utils/co'); /** * Represents an entry in the chain. Unlike @@ -46,7 +49,7 @@ function ChainEntry(chain, options, prev) { return new ChainEntry(chain, options, prev); this.chain = chain; - this.network = chain ? chain.network : bcoin.network.get(); + this.network = chain ? chain.network : Network.primary; this.hash = constants.NULL_HASH; this.version = 1; @@ -156,10 +159,10 @@ ChainEntry.prototype.isGenesis = function isGenesis() { * majority window. These ancestors will be stored * in the `ancestors` array and enable use of synchronous * ChainEntry methods. - * @param {Function} callback + * @returns {Promise} */ -ChainEntry.prototype.getRetargetAncestors = function getRetargetAncestors(callback) { +ChainEntry.prototype.getRetargetAncestors = function getRetargetAncestors() { var majorityWindow = this.network.block.majorityWindow; var medianTimespan = constants.block.MEDIAN_TIMESPAN; var powDiffInterval = this.network.pow.retargetInterval; @@ -167,22 +170,22 @@ ChainEntry.prototype.getRetargetAncestors = function getRetargetAncestors(callba var max = Math.max(majorityWindow, medianTimespan); if ((this.height + 1) % powDiffInterval === 0 || diffReset) max = Math.max(max, powDiffInterval); - this.getAncestors(max, callback); + return this.getAncestors(max); }; /** * Collect ancestors. * @param {Number} max - Number of ancestors. - * @param {Function} callback - Returns [Error, ChainEntry[]]. + * @returns {Promise} - Returns ChainEntry[]. */ -ChainEntry.prototype.getAncestors = function getAncestors(max, callback) { +ChainEntry.prototype.getAncestors = co(function* getAncestors(max) { var entry = this; var ancestors = []; var cached; if (max === 0) - return callback(null, ancestors); + return ancestors; assert(utils.isNumber(max)); @@ -192,7 +195,7 @@ ChainEntry.prototype.getAncestors = function getAncestors(max, callback) { ancestors.push(entry); if (ancestors.length >= max) - return callback(null, ancestors); + return ancestors; cached = this.chain.db.getCache(entry.prevBlock); @@ -204,66 +207,54 @@ ChainEntry.prototype.getAncestors = function getAncestors(max, callback) { entry = cached; } - (function next(err, entry) { - if (err) - return callback(err); - - if (!entry) - return callback(null, ancestors); - + while (entry) { ancestors.push(entry); - if (ancestors.length >= max) - return callback(null, ancestors); + break; + entry = yield entry.getPrevious(); + } - entry.getPrevious(next); - })(null, entry); -}; + return ancestors; +}); /** * Test whether the entry is in the main chain. - * @param {Function} callback - Return [Error, Boolean]. + * @returns {Promise} - Return Boolean. */ -ChainEntry.prototype.isMainChain = function isMainChain(callback) { - this.chain.db.isMainChain(this, callback); +ChainEntry.prototype.isMainChain = function isMainChain() { + return this.chain.db.isMainChain(this); }; /** * Collect ancestors up to `height`. * @param {Number} height - * @param {Function} callback - Returns [Error, ChainEntry[]]. + * @returns {Promise} - Returns ChainEntry[]. */ -ChainEntry.prototype.getAncestorByHeight = function getAncestorByHeight(height, callback) { - var self = this; +ChainEntry.prototype.getAncestorByHeight = co(function* getAncestorByHeight(height) { + var main, entry; if (height < 0) - return utils.nextTick(callback); + return yield co.wait(); assert(height >= 0); assert(height <= this.height); - this.isMainChain(function(err, main) { - if (err) - return callback(err); + main = yield this.isMainChain(); - if (main) - return self.chain.db.get(height, callback); + if (main) + return yield this.chain.db.get(height); - self.getAncestor(self.height - height, function(err, entry) { - if (err) - return callback(err); + entry = yield this.getAncestor(this.height - height); - if (!entry) - return callback(); + if (!entry) + return; - assert(entry.height === height); + assert(entry.height === height); - callback(null, entry); - }); - }); -}; + return entry; +}); /** * Get a single ancestor by index. Note that index-0 is @@ -273,45 +264,39 @@ ChainEntry.prototype.getAncestorByHeight = function getAncestorByHeight(height, * @returns {Function} callback - Returns [Error, ChainEntry]. */ -ChainEntry.prototype.getAncestor = function getAncestor(index, callback) { +ChainEntry.prototype.getAncestor = co(function* getAncestor(index) { + var ancestors; + assert(index >= 0); - this.getAncestors(index + 1, function(err, ancestors) { - if (err) - return callback(err); - if (ancestors.length < index + 1) - return callback(); + ancestors = yield this.getAncestors(index + 1); - callback(null, ancestors[index]); - }); -}; + if (ancestors.length < index + 1) + return; + + return ancestors[index]; +}); /** * Get previous entry. - * @param {Function} callback - Returns [Error, ChainEntry]. + * @returns {Promise} - Returns ChainEntry. */ -ChainEntry.prototype.getPrevious = function getPrevious(callback) { - this.chain.db.get(this.prevBlock, callback); +ChainEntry.prototype.getPrevious = function getPrevious() { + return this.chain.db.get(this.prevBlock); }; /** * Get next entry. - * @param {Function} callback - Returns [Error, ChainEntry]. + * @returns {Promise} - Returns ChainEntry. */ -ChainEntry.prototype.getNext = function getNext(callback) { - var self = this; - this.chain.db.getNextHash(this.hash, function(err, hash) { - if (err) - return callback(err); - - if (!hash) - return callback(); - - self.chain.db.get(hash, callback); - }); -}; +ChainEntry.prototype.getNext = co(function* getNext() { + var hash = yield this.chain.db.getNextHash(this.hash); + if (!hash) + return; + return yield this.chain.db.get(hash); +}); /** * Get median time past. @@ -336,20 +321,14 @@ ChainEntry.prototype.getMedianTime = function getMedianTime(ancestors) { /** * Get median time past asynchronously (see {@link ChainEntry#getMedianTime}). - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ -ChainEntry.prototype.getMedianTimeAsync = function getMedianTimeAsync(callback) { - var self = this; +ChainEntry.prototype.getMedianTimeAsync = co(function* getMedianTimeAsync() { var MEDIAN_TIMESPAN = constants.block.MEDIAN_TIMESPAN; - - this.getAncestors(MEDIAN_TIMESPAN, function(err, ancestors) { - if (err) - return callback(err); - - callback(null, self.getMedianTime(ancestors)); - }); -}; + var ancestors = yield this.getAncestors(MEDIAN_TIMESPAN); + return this.getMedianTime(ancestors); +}); /** * Check isSuperMajority against majorityRejectOutdated. @@ -367,14 +346,13 @@ ChainEntry.prototype.isOutdated = function isOutdated(version, ancestors) { /** * Check {@link ChainEntry#isUpgraded asynchronously}. * @param {Number} version - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. * @returns {Boolean} */ -ChainEntry.prototype.isOutdatedAsync = function isOutdatedAsync(version, callback) { - this.isSuperMajorityAsync(version, - this.network.block.majorityRejectOutdated, - callback); +ChainEntry.prototype.isOutdatedAsync = function isOutdatedAsync(version) { + return this.isSuperMajorityAsync(version, + this.network.block.majorityRejectOutdated); }; /** @@ -393,14 +371,13 @@ ChainEntry.prototype.isUpgraded = function isUpgraded(version, ancestors) { /** * Check {@link ChainEntry#isUpgraded} asynchronously. * @param {Number} version - * @param {Function} callback + * @returns {Promise} * @returns {Boolean} */ -ChainEntry.prototype.isUpgradedAsync = function isUpgradedAsync(version, callback) { - this.isSuperMajorityAsync(version, - this.network.block.majorityEnforceUpgrade, - callback); +ChainEntry.prototype.isUpgradedAsync = function isUpgradedAsync(version) { + return this.isSuperMajorityAsync(version, + this.network.block.majorityEnforceUpgrade); }; /** @@ -430,24 +407,19 @@ ChainEntry.prototype.isSuperMajority = function isSuperMajority(version, require * Calculate {@link ChainEntry#isSuperMajority asynchronously}. * @param {Number} version * @param {Number} required - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. * @returns {Boolean} */ -ChainEntry.prototype.isSuperMajorityAsync = function isSuperMajorityAsync(version, required, callback) { - var self = this; +ChainEntry.prototype.isSuperMajorityAsync = co(function* isSuperMajorityAsync(version, required) { var majorityWindow = this.network.block.majorityWindow; - - this.getAncestors(majorityWindow, function(err, ancestors) { - if (err) - return callback(err); - - callback(null, self.isSuperMajority(version, required, ancestors)); - }); -}; + var ancestors = yield this.getAncestors(majorityWindow); + return this.isSuperMajority(version, required, ancestors); +}); /** - * Test whether the entry is potentially an ancestor of a checkpoint. + * Test whether the entry is potentially + * an ancestor of a checkpoint. * @returns {Boolean} */ @@ -459,6 +431,19 @@ ChainEntry.prototype.isHistorical = function isHistorical() { return false; }; +/** + * Test whether the entry contains a version bit. + * @param {Object} deployment + * @returns {Boolean} + */ + +ChainEntry.prototype.hasBit = function hasBit(deployment) { + var bits = this.version & constants.versionbits.TOP_MASK; + var topBits = constants.versionbits.TOP_BITS; + var mask = 1 << deployment.bit; + return bits === topBits && (this.version & mask) !== 0; +}; + ChainEntry.prototype.__defineGetter__('rhash', function() { return utils.revHex(this.hash); }); @@ -506,7 +491,7 @@ ChainEntry.fromBlock = function fromBlock(chain, block, prev) { ChainEntry.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); - p.write32(this.version); + p.writeU32(this.version); p.writeHash(this.prevBlock); p.writeHash(this.merkleRoot); p.writeU32(this.ts); @@ -624,7 +609,7 @@ ChainEntry.fromJSON = function fromJSON(chain, json) { */ ChainEntry.prototype.toHeaders = function toHeaders() { - return bcoin.headers.fromEntry(this); + return Headers.fromEntry(this); }; /** @@ -633,7 +618,7 @@ ChainEntry.prototype.toHeaders = function toHeaders() { */ ChainEntry.prototype.toInv = function toInv() { - return new bcoin.invitem(constants.inv.BLOCK, this.hash); + return new InvItem(constants.inv.BLOCK, this.hash); }; /** diff --git a/lib/chain/coins.js b/lib/chain/coins.js index dbd15d4d..dd733c09 100644 --- a/lib/chain/coins.js +++ b/lib/chain/coins.js @@ -1,18 +1,20 @@ /*! * coins.js - coins object for bcoin - * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; -var bcoin = require('../env'); -var utils = bcoin.utils; -var assert = utils.assert; -var constants = bcoin.constants; +var utils = require('../utils/utils'); +var assert = require('assert'); +var constants = require('../protocol/constants'); +var Coin = require('../primitives/coin'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); +var compressor = require('./compress'); +var compress = compressor.compress; +var decompress = compressor.decompress; /** * Represents the outputs for a single transaction. @@ -280,7 +282,7 @@ Coins.prototype.toRaw = function toRaw(writer) { continue; } - compressScript(output.script, p); + compress.script(output.script, p); p.writeVarint(output.value); } @@ -449,7 +451,7 @@ Coins.prototype.fromTX = function fromTX(tx) { this.outputs.push(null); continue; } - this.outputs.push(bcoin.coin.fromTX(tx, i)); + this.outputs.push(Coin.fromTX(tx, i)); } return this; @@ -502,7 +504,7 @@ function CompressedCoin(offset, size, raw) { CompressedCoin.prototype.toCoin = function toCoin(coins, index) { var p = new BufferReader(this.raw); - var coin = new bcoin.coin(); + var coin = new Coin(); // Load in all necessary properties // from the parent Coins object. @@ -515,7 +517,7 @@ CompressedCoin.prototype.toCoin = function toCoin(coins, index) { // Seek to the coin's offset. p.seek(this.offset); - decompressScript(p, coin.script); + decompress.script(p, coin.script); coin.value = p.readVarint(); @@ -532,236 +534,8 @@ CompressedCoin.prototype.toRaw = function toRaw() { return this.raw.slice(this.offset, this.offset + this.size); }; -/* - * Compression - */ - -/** - * Compress a script, write directly to the buffer. - * @param {Script} script - * @param {BufferWriter} p - */ - -function compressScript(script, p) { - var prefix = 0; - var data; - - // Attempt to compress the output scripts. - // We can _only_ ever compress them if - // they are serialized as minimaldata, as - // we need to recreate them when we read - // them. - if (script.isPubkeyhash(true)) { - prefix = 1; - data = script.code[2].data; - } else if (script.isScripthash()) { - prefix = 2; - data = script.code[1].data; - } else if (script.isPubkey(true)) { - prefix = 3; - data = script.code[0].data; - - // Try to compress the key. - data = compressKey(data); - - // If we can't compress it, - // just store the script. - if (!data) - prefix = 0; - } - - p.writeU8(prefix); - - if (prefix === 0) - p.writeVarBytes(script.toRaw()); - else - p.writeBytes(data); -} - -/** - * Decompress a script from buffer reader. - * @param {BufferReader} p - * @param {Script} script - */ - -function decompressScript(p, script) { - var key; - - // Decompress the script. - switch (p.readU8()) { - case 0: - script.fromRaw(p.readVarBytes()); - break; - case 1: - script.fromPubkeyhash(p.readBytes(20)); - break; - case 2: - script.fromScripthash(p.readBytes(20)); - break; - case 3: - // Decompress the key. If this fails, - // we have database corruption! - key = decompressKey(p.readBytes(33)); - script.fromPubkey(key); - break; - default: - throw new Error('Bad prefix.'); - } -} - -/** - * Compress value using an exponent. Takes advantage of - * the fact that many bitcoin values are divisible by 10. - * @see https://github.com/btcsuite/btcd/blob/master/blockchain/compress.go - * @param {Amount} value - * @returns {Number} - */ - -function compressValue(value) { - var exp, last; - - if (value === 0) - return 0; - - exp = 0; - while (value % 10 === 0 && exp < 9) { - value /= 10; - exp++; - } - - if (exp < 9) { - last = value % 10; - value = (value - last) / 10; - return 1 + 10 * (9 * value + last - 1) + exp; - } - - return 10 + 10 * (value - 1); -} - -/** - * Decompress value. - * @param {Number} value - Compressed value. - * @returns {Amount} value - */ - -function decompressValue(value) { - var exp, n, last; - - if (value === 0) - return 0; - - value--; - - exp = value % 10; - value = (value - exp) / 10; - - if (exp < 9) { - last = value % 9; - value = (value - last) / 9; - n = value * 10 + last + 1; - } else { - n = value + 1; - } - - while (exp > 0) { - n *= 10; - exp--; - } - - return n; -} - -/** - * Compress a public key to coins compression format. - * @param {Buffer} key - * @returns {Buffer} - */ - -function compressKey(key) { - var out; - - // We can't compress it if it's not valid. - if (!bcoin.ec.publicKeyVerify(key)) - return; - - switch (key[0]) { - case 0x02: - case 0x03: - // Key is already compressed. - out = key; - break; - case 0x04: - case 0x06: - case 0x07: - // Compress the key normally. - out = bcoin.ec.publicKeyConvert(key, true); - // Store the original format (which - // may be a hybrid byte) in the hi - // 3 bits so we can restore it later. - // The hi bits being set also lets us - // know that this key was originally - // decompressed. - out[0] |= key[0] << 2; - break; - default: - throw new Error('Bad point format.'); - } - - assert(out.length === 33); - - return out; -} - -/** - * Decompress a public key from the coins compression format. - * @param {Buffer} key - * @returns {Buffer} - */ - -function decompressKey(key) { - var format = key[0] >>> 2; - var out; - - assert(key.length === 33); - - // Hi bits are not set. This key - // is not meant to be decompressed. - if (format === 0) - return key; - - // Decompress the key, and off the - // low bits so publicKeyConvert - // actually understands it. - key[0] &= 0x03; - out = bcoin.ec.publicKeyConvert(key, false); - - // Reset the hi bits so as not to - // mutate the original buffer. - key[0] |= format << 2; - - // Set the original format, which - // may have been a hybrid prefix byte. - out[0] = format; - - return out; -} - /* * Expose */ -exports = Coins; - -exports.compress = { - script: compressScript, - value: compressValue, - key: compressKey -}; - -exports.decompress = { - script: decompressScript, - value: decompressValue, - key: decompressKey -}; - -module.exports = exports; +module.exports = Coins; diff --git a/lib/chain/coinview.js b/lib/chain/coinview.js index 61d4f57f..a7cb371f 100644 --- a/lib/chain/coinview.js +++ b/lib/chain/coinview.js @@ -1,15 +1,13 @@ /*! * coinview.js - coinview object for bcoin - * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; -var bcoin = require('../env'); -var utils = bcoin.utils; -var assert = utils.assert; +var assert = require('assert'); +var Coins = require('./coins'); /** * A collections of {@link Coins} objects. @@ -43,7 +41,7 @@ CoinView.prototype.add = function add(coins) { CoinView.prototype.addCoin = function addCoin(coin) { assert(typeof coin.hash === 'string'); if (!this.coins[coin.hash]) - this.coins[coin.hash] = new bcoin.coins(); + this.coins[coin.hash] = new Coins(); this.coins[coin.hash].add(coin); }; @@ -53,7 +51,7 @@ CoinView.prototype.addCoin = function addCoin(coin) { */ CoinView.prototype.addTX = function addTX(tx) { - this.add(bcoin.coins.fromTX(tx)); + this.add(Coins.fromTX(tx)); }; /** diff --git a/lib/chain/compress.js b/lib/chain/compress.js new file mode 100644 index 00000000..963fc8fe --- /dev/null +++ b/lib/chain/compress.js @@ -0,0 +1,240 @@ +/*! + * compress.js - coin compressor for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var assert = require('assert'); +var ec = require('../crypto/ec'); + +/* + * Compression + */ + +/** + * Compress a script, write directly to the buffer. + * @param {Script} script + * @param {BufferWriter} p + */ + +function compressScript(script, p) { + var prefix = 0; + var data; + + // Attempt to compress the output scripts. + // We can _only_ ever compress them if + // they are serialized as minimaldata, as + // we need to recreate them when we read + // them. + if (script.isPubkeyhash(true)) { + prefix = 1; + data = script.code[2].data; + } else if (script.isScripthash()) { + prefix = 2; + data = script.code[1].data; + } else if (script.isPubkey(true)) { + prefix = 3; + data = script.code[0].data; + + // Try to compress the key. + data = compressKey(data); + + // If we can't compress it, + // just store the script. + if (!data) + prefix = 0; + } + + p.writeU8(prefix); + + if (prefix === 0) + p.writeVarBytes(script.toRaw()); + else + p.writeBytes(data); +} + +/** + * Decompress a script from buffer reader. + * @param {BufferReader} p + * @param {Script} script + */ + +function decompressScript(p, script) { + var key; + + // Decompress the script. + switch (p.readU8()) { + case 0: + script.fromRaw(p.readVarBytes()); + break; + case 1: + script.fromPubkeyhash(p.readBytes(20)); + break; + case 2: + script.fromScripthash(p.readBytes(20)); + break; + case 3: + // Decompress the key. If this fails, + // we have database corruption! + key = decompressKey(p.readBytes(33)); + script.fromPubkey(key); + break; + default: + throw new Error('Bad prefix.'); + } +} + +/** + * Compress value using an exponent. Takes advantage of + * the fact that many bitcoin values are divisible by 10. + * @see https://github.com/btcsuite/btcd/blob/master/blockchain/compress.go + * @param {Amount} value + * @returns {Number} + */ + +function compressValue(value) { + var exp, last; + + if (value === 0) + return 0; + + exp = 0; + while (value % 10 === 0 && exp < 9) { + value /= 10; + exp++; + } + + if (exp < 9) { + last = value % 10; + value = (value - last) / 10; + return 1 + 10 * (9 * value + last - 1) + exp; + } + + return 10 + 10 * (value - 1); +} + +/** + * Decompress value. + * @param {Number} value - Compressed value. + * @returns {Amount} value + */ + +function decompressValue(value) { + var exp, n, last; + + if (value === 0) + return 0; + + value--; + + exp = value % 10; + value = (value - exp) / 10; + + if (exp < 9) { + last = value % 9; + value = (value - last) / 9; + n = value * 10 + last + 1; + } else { + n = value + 1; + } + + while (exp > 0) { + n *= 10; + exp--; + } + + return n; +} + +/** + * Compress a public key to coins compression format. + * @param {Buffer} key + * @returns {Buffer} + */ + +function compressKey(key) { + var out; + + // We can't compress it if it's not valid. + if (!ec.publicKeyVerify(key)) + return; + + switch (key[0]) { + case 0x02: + case 0x03: + // Key is already compressed. + out = key; + break; + case 0x04: + case 0x06: + case 0x07: + // Compress the key normally. + out = ec.publicKeyConvert(key, true); + // Store the original format (which + // may be a hybrid byte) in the hi + // 3 bits so we can restore it later. + // The hi bits being set also lets us + // know that this key was originally + // decompressed. + out[0] |= key[0] << 2; + break; + default: + throw new Error('Bad point format.'); + } + + assert(out.length === 33); + + return out; +} + +/** + * Decompress a public key from the coins compression format. + * @param {Buffer} key + * @returns {Buffer} + */ + +function decompressKey(key) { + var format = key[0] >>> 2; + var out; + + assert(key.length === 33); + + // Hi bits are not set. This key + // is not meant to be decompressed. + if (format === 0) + return key; + + // Decompress the key, and off the + // low bits so publicKeyConvert + // actually understands it. + key[0] &= 0x03; + out = ec.publicKeyConvert(key, false); + + // Reset the hi bits so as not to + // mutate the original buffer. + key[0] |= format << 2; + + // Set the original format, which + // may have been a hybrid prefix byte. + out[0] = format; + + return out; +} + +/* + * Expose + */ + +exports.compress = { + script: compressScript, + value: compressValue, + key: compressKey +}; + +exports.decompress = { + script: decompressScript, + value: decompressValue, + key: decompressKey +}; diff --git a/lib/chain/index.js b/lib/chain/index.js new file mode 100644 index 00000000..cfe2e3f2 --- /dev/null +++ b/lib/chain/index.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.Chain = require('./chain'); +exports.ChainDB = require('./chaindb'); +exports.ChainEntry = require('./chainentry'); +exports.Coins = require('./coins'); +exports.CoinView = require('./coinview'); +exports.compressor = require('./compress'); diff --git a/lib/crypto/aes.js b/lib/crypto/aes.js index 26b71215..f7ac9523 100644 --- a/lib/crypto/aes.js +++ b/lib/crypto/aes.js @@ -490,7 +490,7 @@ AESCipher.prototype.update = function update(data) { var i, len, trailing, block; if (this.waiting) { - data = Buffer.concat([this.waiting, data]); + data = concat(this.waiting, data); this.waiting = null; } @@ -529,7 +529,7 @@ AESCipher.prototype.final = function final() { left = 16 - block.length; pad = new Buffer(left); pad.fill(left); - block = Buffer.concat([block, pad]); + block = concat(block, pad); } // Encrypt the last block, @@ -577,7 +577,7 @@ AESDecipher.prototype.update = function update(data) { var i, chunk, block, len, trailing; if (this.waiting) { - data = Buffer.concat([this.waiting, data]); + data = concat(this.waiting, data); this.waiting = null; } @@ -656,10 +656,7 @@ AESDecipher.prototype.final = function final() { AES.encrypt = function encrypt(data, key, iv, bits, mode) { var cipher = new AESCipher(key, iv, bits, mode); - return Buffer.concat([ - cipher.update(data), - cipher.final() - ]); + return concat(cipher.update(data), cipher.final()); }; /** @@ -674,10 +671,7 @@ AES.encrypt = function encrypt(data, key, iv, bits, mode) { AES.decrypt = function decrypt(data, key, iv, bits, mode) { var decipher = new AESDecipher(key, iv, bits, mode); - return Buffer.concat([ - decipher.update(data), - decipher.final() - ]); + return concat(decipher.update(data), decipher.final()); }; /** @@ -771,6 +765,13 @@ function writeU32(data, value, i) { data[i + 3] = value & 0xff; } +function concat(a, b) { + var data = new Buffer(a.length + b.length); + a.copy(data, 0); + b.copy(data, a.length); + return data; +} + /* * Tables */ diff --git a/lib/crypto/crypto.js b/lib/crypto/crypto.js index 7033ce3f..09b6388f 100644 --- a/lib/crypto/crypto.js +++ b/lib/crypto/crypto.js @@ -12,15 +12,17 @@ var random = require('./random'); var scrypt = require('./scrypt'); var scryptAsync = require('./scrypt-async'); var utils = require('../utils/utils'); +var co = require('../utils/co'); var native = require('../utils/native'); -var nativeCrypto, hash, aes; +var lazy = require('../utils/lazy')(require, exports); +var nodeCrypto, hash, aes; var isBrowser = (typeof process !== 'undefined' && process.browser) || typeof window !== 'undefined'; if (!isBrowser) { - nativeCrypto = require('crypto'); + nodeCrypto = require('crypto'); } else { hash = require('hash.js'); aes = require('./aes'); @@ -40,10 +42,10 @@ var crypto = exports; */ crypto.hash = function _hash(alg, data) { - if (!nativeCrypto) + if (!nodeCrypto) return new Buffer(hash[alg]().update(data).digest()); - return nativeCrypto.createHash(alg).update(data).digest(); + return nodeCrypto.createHash(alg).update(data).digest(); }; if (native) @@ -129,12 +131,12 @@ crypto.checksum = function checksum(data) { crypto.hmac = function hmac(alg, data, salt) { var hmac; - if (!nativeCrypto) { + if (!nodeCrypto) { hmac = hash.hmac(hash[alg], salt); return new Buffer(hmac.update(data).digest()); } - hmac = nativeCrypto.createHmac(alg, salt); + hmac = nodeCrypto.createHmac(alg, salt); return hmac.update(data).digest(); }; @@ -158,8 +160,8 @@ crypto.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) { if (typeof salt === 'string') salt = new Buffer(salt, 'utf8'); - if (nativeCrypto && nativeCrypto.pbkdf2Sync) - return nativeCrypto.pbkdf2Sync(key, salt, iter, len, alg); + if (nodeCrypto && nodeCrypto.pbkdf2Sync) + return nodeCrypto.pbkdf2Sync(key, salt, iter, len, alg); return crypto._pbkdf2(key, salt, iter, len, alg); }; @@ -171,10 +173,10 @@ crypto.pbkdf2 = function pbkdf2(key, salt, iter, len, alg) { * @param {Number} iter * @param {Number} len * @param {String} alg - * @param {Function} callback + * @returns {Promise} */ -crypto.pbkdf2Async = function pbkdf2Async(key, salt, iter, len, alg, callback) { +crypto.pbkdf2Async = function pbkdf2Async(key, salt, iter, len, alg) { var result; if (typeof key === 'string') @@ -183,16 +185,19 @@ crypto.pbkdf2Async = function pbkdf2Async(key, salt, iter, len, alg, callback) { if (typeof salt === 'string') salt = new Buffer(salt, 'utf8'); - if (nativeCrypto && nativeCrypto.pbkdf2) - return nativeCrypto.pbkdf2(key, salt, iter, len, alg, callback); + if (nodeCrypto && nodeCrypto.pbkdf2) { + return new Promise(function(resolve, reject) { + nodeCrypto.pbkdf2(key, salt, iter, len, alg, co.wrap(resolve, reject)); + }); + } try { result = crypto._pbkdf2(key, salt, iter, len, alg); } catch (e) { - return callback(e); + return Promise.reject(e); } - return callback(null, result); + return Promise.resolve(result); }; /** @@ -224,27 +229,29 @@ crypto.scrypt = function _scrypt(passwd, salt, N, r, p, len) { * @param {Number} r * @param {Number} p * @param {Number} len - * @param {Function} callback + * @returns {Promise} */ -crypto.scryptAsync = function _scrypt(passwd, salt, N, r, p, len, callback) { +crypto.scryptAsync = function _scrypt(passwd, salt, N, r, p, len) { if (typeof passwd === 'string') passwd = new Buffer(passwd, 'utf8'); if (typeof salt === 'string') salt = new Buffer(salt, 'utf8'); - return scryptAsync(passwd, salt, N, r, p, len, callback); + return new Promise(function(resolve, reject) { + scryptAsync(passwd, salt, N, r, p, len, co.wrap(resolve, reject)); + }); }; /** * Derive a key using pbkdf2 with 50,000 iterations. * @param {Buffer|String} passphrase - * @param {Function} callback + * @returns {Promise} */ -crypto.derive = function derive(passphrase, callback) { - crypto.pbkdf2Async(passphrase, 'bcoin', 50000, 32, 'sha256', callback); +crypto.derive = function derive(passphrase) { + return crypto.pbkdf2Async(passphrase, 'bcoin', 50000, 32, 'sha256'); }; /** @@ -252,30 +259,29 @@ crypto.derive = function derive(passphrase, callback) { * @param {Buffer} data * @param {Buffer|String} passphrase * @param {Buffer} iv - 128 bit initialization vector. - * @param {Function} callback + * @returns {Promise} */ -crypto.encrypt = function encrypt(data, passphrase, iv, callback) { +crypto.encrypt = co(function* encrypt(data, passphrase, iv) { + var key; + assert(Buffer.isBuffer(data)); assert(passphrase, 'No passphrase.'); assert(Buffer.isBuffer(iv)); - crypto.derive(passphrase, function(err, key) { - if (err) - return callback(err); - - try { - data = crypto.encipher(data, key, iv); - } catch (e) { - key.fill(0); - return callback(e); - } + key = yield crypto.derive(passphrase); + try { + data = crypto.encipher(data, key, iv); + } catch (e) { key.fill(0); + throw e; + } - return callback(null, data); - }); -}; + key.fill(0); + + return data; +}); /** * Encrypt with aes-256-cbc. @@ -288,15 +294,12 @@ crypto.encrypt = function encrypt(data, passphrase, iv, callback) { crypto.encipher = function encipher(data, key, iv) { var cipher; - if (!nativeCrypto) + if (!nodeCrypto) return aes.cbc.encrypt(data, key, iv); - cipher = nativeCrypto.createCipheriv('aes-256-cbc', key, iv); + cipher = nodeCrypto.createCipheriv('aes-256-cbc', key, iv); - return Buffer.concat([ - cipher.update(data), - cipher.final() - ]); + return utils.concat(cipher.update(data), cipher.final()); }; /** @@ -304,27 +307,29 @@ crypto.encipher = function encipher(data, key, iv) { * @param {Buffer} data * @param {Buffer|String} passphrase * @param {Buffer} iv - 128 bit initialization vector. - * @param {Function} callback + * @returns {Promise} */ -crypto.decrypt = function decrypt(data, passphrase, iv, callback) { +crypto.decrypt = co(function* decrypt(data, passphrase, iv) { + var key; + assert(Buffer.isBuffer(data)); assert(passphrase, 'No passphrase.'); assert(Buffer.isBuffer(iv)); - crypto.derive(passphrase, function(err, key) { - if (err) - return callback(err); + key = yield crypto.derive(passphrase); - try { - data = crypto.decipher(data, key, iv); - } catch (e) { - return callback(e); - } + try { + data = crypto.decipher(data, key, iv); + } catch (e) { + key.fill(0); + throw e; + } - return callback(null, data, key); - }); -}; + key.fill(0); + + return data; +}); /** * Decrypt with aes-256-cbc. @@ -337,15 +342,12 @@ crypto.decrypt = function decrypt(data, passphrase, iv, callback) { crypto.decipher = function decipher(data, key, iv) { var decipher; - if (!nativeCrypto) + if (!nodeCrypto) return aes.cbc.decrypt(data, key, iv); - decipher = nativeCrypto.createDecipheriv('aes-256-cbc', key, iv); + decipher = nodeCrypto.createDecipheriv('aes-256-cbc', key, iv); - return Buffer.concat([ - decipher.update(data), - decipher.final() - ]); + return utils.concat(decipher.update(data), decipher.final()); }; /** @@ -632,3 +634,13 @@ crypto.randomRange = random.randomRange; */ crypto.randomInt = random.randomInt; + +/* + * Expose other objects. + */ + +lazy('aes', './aes'); +lazy('chachapoly', './chachapoly'); +lazy('ec', './ec'); +lazy('schnorr', './schnorr'); +lazy('siphash', './siphash'); diff --git a/lib/crypto/ec.js b/lib/crypto/ec.js index d159151e..2727b303 100644 --- a/lib/crypto/ec.js +++ b/lib/crypto/ec.js @@ -11,7 +11,7 @@ var elliptic = require('elliptic'); var bn = require('bn.js'); var utils = require('../utils/utils'); var crypto = require('./crypto'); -var assert = utils.assert; +var assert = require('assert'); var secp256k1; try { @@ -21,6 +21,19 @@ try { ; } +/* + * Constants + */ + +var ZERO_S = new Buffer( + '0000000000000000000000000000000000000000000000000000000000000000', + 'hex' +); + +var HALF_ORDER = new Buffer( + '7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0', + 'hex'); + /** * @exports ec */ @@ -173,8 +186,7 @@ ec.ecdh = function ecdh(pub, priv) { if (secp256k1) { point = secp256k1.ecdhUnsafe(pub, priv, true); - point = ec.curve.decodePoint(point); - return point.getX().toArrayLike(Buffer, 'be', 32); + return point.slice(1, 33); } priv = ec.elliptic.keyPair({ priv: priv }); @@ -204,11 +216,13 @@ ec.recover = function recover(msg, sig, j, compressed) { } catch (e) { return; } + try { key = secp256k1.recover(msg, sig, j, compressed); } catch (e) { return; } + return key; } @@ -248,22 +262,16 @@ ec.verify = function verify(msg, sig, key, historical, high) { if (key.length === 0) return false; - // Attempt to normalize the signature - // length before passing to elliptic. - // Note: We only do this for historical data! - // https://github.com/indutny/elliptic/issues/78 - if (historical) - sig = ec.normalizeLength(sig); - if (secp256k1) { - // secp256k1 fails on high s values. This is - // bad for verifying historical data. - if (high) - sig = ec.toLowS(sig); - try { - // Import from DER. - sig = secp256k1.signatureImport(sig); + if (historical) + sig = secp256k1.signatureImportLax(sig); + else + sig = secp256k1.signatureImport(sig); + + if (high) + sig = secp256k1.signatureNormalize(sig); + result = secp256k1.verify(msg, sig, key); } catch (e) { result = false; @@ -272,6 +280,13 @@ ec.verify = function verify(msg, sig, key, historical, high) { return result; } + // Attempt to normalize the signature + // length before passing to elliptic. + // Note: We only do this for historical data! + // https://github.com/indutny/elliptic/issues/78 + if (historical) + sig = ec.normalizeLength(sig); + // Make elliptic mimic secp256k1's // failure with high S values. if (!high && !ec.isLowS(sig)) @@ -446,14 +461,36 @@ ec.normalizeLength = function normalizeLength(sig) { */ ec.isLowS = function isLowS(sig) { - if (Buffer.isBuffer(sig)) { + var rs, s; + + if (secp256k1) { try { - sig = new ec.signature(sig); + rs = secp256k1.signatureImport(sig); + s = rs.slice(32, 64); } catch (e) { return false; } + + if (utils.equal(s, ZERO_S)) + return false; + + // If S is greater than half the order, + // it's too high. + if (utils.cmp(s, HALF_ORDER) > 0) + return false; + + return true; } + try { + sig = new ec.signature(sig); + } catch (e) { + return false; + } + + if (sig.s.cmpn(0) === 0) + return false; + // If S is greater than half the order, // it's too high. if (sig.s.cmp(ec.elliptic.nh) > 0) @@ -462,30 +499,6 @@ ec.isLowS = function isLowS(sig) { return true; }; -/** - * Lower the S value of a signature (used - * for verifying historical data). - * @param {Buffer} sig - DER formatted. - * @returns {Buffer} - */ - -ec.toLowS = function toLowS(sig) { - if (Buffer.isBuffer(sig)) { - try { - sig = new ec.signature(sig); - } catch (e) { - return sig; - } - } - - // If S is greater than half the order, - // it's too high. - if (sig.s.cmp(ec.elliptic.nh) > 0) - sig.s = ec.curve.n.sub(sig.s); - - return new Buffer(sig.toDER()); -}; - /* * Helpers */ diff --git a/lib/crypto/index.js b/lib/crypto/index.js new file mode 100644 index 00000000..7959f4cc --- /dev/null +++ b/lib/crypto/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./crypto'); diff --git a/lib/db/index.js b/lib/db/index.js new file mode 100644 index 00000000..fba991a5 --- /dev/null +++ b/lib/db/index.js @@ -0,0 +1,11 @@ +'use strict'; + +var utils = require('../utils/utils'); + +exports.ldb = require('./ldb'); + +if (utils.isBrowser) + exports.level = require('./level'); + +exports.LowlevelUp = require('./lowlevelup'); +exports.RBT = require('./rbt'); diff --git a/lib/db/ldb.js b/lib/db/ldb.js index 5376893b..6bdd8970 100644 --- a/lib/db/ldb.js +++ b/lib/db/ldb.js @@ -11,7 +11,7 @@ var LowlevelUp = require('./lowlevelup'); var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); /** * @param {Object} options diff --git a/lib/db/lowlevelup.js b/lib/db/lowlevelup.js index ec0e8501..0626f947 100644 --- a/lib/db/lowlevelup.js +++ b/lib/db/lowlevelup.js @@ -8,8 +8,9 @@ 'use strict'; var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); var AsyncObject = require('../utils/async'); +var co = require('../utils/co'); var VERSION_ERROR; /** @@ -57,148 +58,193 @@ utils.inherits(LowlevelUp, AsyncObject); /** * Open the database (recallable). * @alias LowlevelUp#open - * @param {Function} callback + * @returns {Promise} */ -LowlevelUp.prototype._open = function open(callback) { - this.binding.open(this.options, callback); +LowlevelUp.prototype._open = function open() { + var self = this; + return new Promise(function(resolve, reject) { + self.binding.open(self.options, co.wrap(resolve, reject)); + }); }; /** * Close the database (recallable). * @alias LowlevelUp#close - * @param {Function} callback + * @returns {Promise} */ -LowlevelUp.prototype._close = function close(callback) { - this.binding.close(callback); +LowlevelUp.prototype._close = function close() { + var self = this; + return new Promise(function(resolve, reject) { + self.binding.close(co.wrap(resolve, reject)); + }); }; /** * Destroy the database. - * @param {Function} callback + * @returns {Promise} */ -LowlevelUp.prototype.destroy = function destroy(callback) { +LowlevelUp.prototype.destroy = function destroy() { + var self = this; + assert(!this.loading); assert(!this.closing); assert(!this.loaded); - if (!this.backend.destroy) - return utils.asyncify(callback)(new Error('Cannot destroy.')); - - this.backend.destroy(this.location, callback); + return new Promise(function(resolve, reject) { + if (!self.backend.destroy) + return reject(new Error('Cannot destroy.')); + self.backend.destroy(self.location, co.wrap(resolve, reject)); + }); }; /** * Repair the database. - * @param {Function} callback + * @returns {Promise} */ -LowlevelUp.prototype.repair = function repair(callback) { +LowlevelUp.prototype.repair = function repair() { + var self = this; + assert(!this.loading); assert(!this.closing); assert(!this.loaded); - if (!this.backend.repair) - return utils.asyncify(callback)(new Error('Cannot repair.')); - - this.backend.repair(this.location, callback); + return new Promise(function(resolve, reject) { + if (!self.backend.repair) + return reject(new Error('Cannot repair.')); + self.backend.repair(self.location, co.wrap(resolve, reject)); + }); }; /** * Backup the database. * @param {String} path - * @param {Function} callback + * @returns {Promise} */ -LowlevelUp.prototype.backup = function backup(path, callback) { +LowlevelUp.prototype.backup = function backup(path) { + var self = this; + assert(!this.loading); assert(!this.closing); assert(this.loaded); if (!this.binding.backup) - return this.clone(path, callback); + return this.clone(path); - this.binding.backup(path, callback); + return new Promise(function(resolve, reject) { + self.binding.backup(path, co.wrap(resolve, reject)); + }); }; /** * Retrieve a record from the database. - * @param {String} key - * @param {Object?} options - * @param {Function} callback - Returns [Error, Buffer]. + * @param {String|Buffer} key + * @returns {Promise} - Returns Buffer. */ -LowlevelUp.prototype.get = function get(key, options, callback) { +LowlevelUp.prototype.get = function get(key) { + var self = this; + assert(this.loaded, 'Cannot use database before it is loaded.'); - if (typeof options === 'function') { - callback = options; - options = {}; - } - - this.binding.get(key, options, function(err, result) { - if (err) { - if (isNotFound(err)) - return callback(); - return callback(err); - } - return callback(null, result); + return new Promise(function(resolve, reject) { + self.binding.get(key, function(err, result) { + if (err) { + if (isNotFound(err)) + return resolve(); + return reject(err); + } + return resolve(result); + }); }); }; /** * Store a record in the database. - * @param {String} key + * @param {String|Buffer} key * @param {Buffer} value - * @param {Object?} options - * @param {Function} callback + * @returns {Promise} */ -LowlevelUp.prototype.put = function put(key, value, options, callback) { +LowlevelUp.prototype.put = function put(key, value) { + var self = this; assert(this.loaded, 'Cannot use database before it is loaded.'); - this.binding.put(key, value, options, callback); + return new Promise(function(resolve, reject) { + self.binding.put(key, value, co.wrap(resolve, reject)); + }); }; /** * Remove a record from the database. - * @param {String} key - * @param {Object?} options - * @param {Function} callback + * @param {String|Buffer} key + * @returns {Promise} */ -LowlevelUp.prototype.del = function del(key, options, callback) { +LowlevelUp.prototype.del = function del(key) { + var self = this; assert(this.loaded, 'Cannot use database before it is loaded.'); - this.binding.del(key, options, callback); + return new Promise(function(resolve, reject) { + self.binding.del(key, co.wrap(resolve, reject)); + }); }; /** * Create an atomic batch. * @param {Array?} ops - * @param {Object?} options - * @param {Function} callback - * @returns {Leveldown.Batch} + * @returns {Batch} */ -LowlevelUp.prototype.batch = function batch(ops, options, callback) { +LowlevelUp.prototype.batch = function batch(ops) { + var self = this; + assert(this.loaded, 'Cannot use database before it is loaded.'); if (!ops) - return this.binding.batch(); + return new Batch(this); - this.binding.batch(ops, options, callback); + return new Promise(function(resolve, reject) { + self.binding.batch(ops, co.wrap(resolve, reject)); + }); }; /** * Create an iterator. * @param {Object} options - * @returns {Leveldown.Iterator} + * @returns {Iterator} */ LowlevelUp.prototype.iterator = function iterator(options) { + var opt; + assert(this.loaded, 'Cannot use database before it is loaded.'); - return this.db.iterator(options); + + opt = { + gte: options.gte, + lte: options.lte, + keys: options.keys !== false, + values: options.values || false, + fillCache: options.fillCache || false, + keyAsBuffer: this.bufferKeys, + valueAsBuffer: true, + reverse: options.reverse || false, + highWaterMark: options.highWaterMark || 16 * 1024 + }; + + // Workaround for a leveldown + // bug I haven't fixed yet. + if (options.limit != null) + opt.limit = options.limit; + + if (options.keyAsBuffer != null) + opt.keyAsBuffer = options.keyAsBuffer; + + assert(opt.keys || opt.values, 'Keys and/or values must be chosen.'); + + return new Iterator(this, opt); }; /** @@ -220,199 +266,215 @@ LowlevelUp.prototype.getProperty = function getProperty(name) { * Calculate approximate database size. * @param {String} start - Start key. * @param {String} end - End key. - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ -LowlevelUp.prototype.approximateSize = function approximateSize(start, end, callback) { +LowlevelUp.prototype.approximateSize = function approximateSize(start, end) { + var self = this; + assert(this.loaded, 'Cannot use database before it is loaded.'); - if (!this.binding.approximateSize) - return utils.asyncify(callback)(new Error('Cannot get size.')); + return new Promise(function(resolve, reject) { + if (!self.binding.approximateSize) + return reject(new Error('Cannot get size.')); - this.binding.approximateSize(start, end, callback); + self.binding.approximateSize(start, end, co.wrap(resolve, reject)); + }); }; /** * Test whether a key exists. * @param {String} key - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -LowlevelUp.prototype.has = function has(key, callback) { - this.get(key, function(err, value) { - if (err) - return callback(err); - - return callback(null, value != null); - }); -}; - -/** - * Get and deserialize a record with a callback. - * @param {String} key - * @param {Function} parse - Accepts [Buffer(data), String(key)]. - * Return value should be the parsed object. - * @param {Function} callback - Returns [Error, Object]. - */ - -LowlevelUp.prototype.fetch = function fetch(key, parse, callback) { - this.get(key, function(err, value) { - if (err) - return callback(err); - - if (!value) - return callback(); - - try { - value = parse(value, key); - } catch (e) { - return callback(e); - } - - return callback(null, value); - }); -}; - -/** - * Iterate over each record. - * @param {Object} options - * @param {Function} handler - * @param {Function} callback - Returns [Error, Object]. - */ - -LowlevelUp.prototype.each = function each(options, handler, callback) { - var i = 0; - var opt, iter; - - opt = { - gte: options.gte, - lte: options.lte, - keys: options.keys !== false, - values: options.values || false, - fillCache: options.fillCache || false, - keyAsBuffer: this.bufferKeys, - valueAsBuffer: true, - reverse: options.reverse || false - }; - - // Workaround for a leveldown - // bug I haven't fixed yet. - if (options.limit != null) - opt.limit = options.limit; - - if (options.keyAsBuffer != null) - opt.keyAsBuffer = options.keyAsBuffer; - - assert(opt.keys || opt.values, 'Keys and/or values must be chosen.'); - - iter = this.iterator(opt); - - function next(err, key) { - if (err && typeof err !== 'boolean') { - return iter.end(function() { - callback(err); - }); - } - - if (err === false) - return iter.end(callback); - - if (err === true) { - try { - iter.seek(key); - } catch (e) { - return iter.end(function() { - callback(e); - }); - } - } - - iter.next(onNext); - } - - function onNext(err, key, value) { - if (err) { - return iter.end(function() { - callback(err); - }); - } - - if (key === undefined && value === undefined) - return iter.end(callback); - - try { - handler(key, value, next, i++); - } catch (e) { - return iter.end(function() { - callback(e); - }); - } - } - - next(); -}; +LowlevelUp.prototype.has = co(function* has(key) { + var value = yield this.get(key); + return value != null; +}); /** * Collect all keys from iterator options. * @param {Object} options - Iterator options. - * @param {Function} callback - Returns [Error, Array]. + * @returns {Promise} - Returns Array. */ -LowlevelUp.prototype.iterate = function iterate(options, callback) { +LowlevelUp.prototype.range = co(function* range(options) { var items = []; - assert(typeof options.parse === 'function', 'Parse must be a function.'); - this.each(options, function(key, value, next) { - var result = options.parse(key, value); - if (result) - items.push(result); - next(); - }, function(err) { - if (err) - return callback(err); - callback(null, items); + var parse = options.parse; + var iter, item; + + iter = this.iterator({ + gte: options.gte, + lte: options.lte, + keys: true, + values: true }); -}; + + for (;;) { + item = yield iter.next(); + + if (!item) + break; + + if (parse) { + try { + item = parse(item.key, item.value); + } catch (e) { + yield iter.end(); + throw e; + } + } + + if (item) + items.push(item); + } + + return items; +}); + +/** + * Collect all keys from iterator options. + * @param {Object} options - Iterator options. + * @returns {Promise} - Returns Array. + */ + +LowlevelUp.prototype.keys = co(function* keys(options) { + var keys = []; + var parse = options.parse; + var iter, item, key; + + iter = this.iterator({ + gte: options.gte, + lte: options.lte, + keys: true, + values: false + }); + + for (;;) { + item = yield iter.next(); + + if (!item) + break; + + key = item.key; + + if (parse) { + try { + key = parse(key); + } catch (e) { + yield iter.end(); + throw e; + } + } + + if (key) + keys.push(key); + } + + return keys; +}); + +/** + * Collect all keys from iterator options. + * @param {Object} options - Iterator options. + * @returns {Promise} - Returns Array. + */ + +LowlevelUp.prototype.values = co(function* values(options) { + var values = []; + var parse = options.parse; + var iter, item, value; + + iter = this.iterator({ + gte: options.gte, + lte: options.lte, + keys: false, + values: true + }); + + for (;;) { + item = yield iter.next(); + + if (!item) + break; + + value = item.value; + + if (parse) { + try { + value = parse(value); + } catch (e) { + yield iter.end(); + throw e; + } + } + + if (value) + values.push(value); + } + + return values; +}); + +/** + * Dump database (for debugging). + * @returns {Promise} - Returns Object. + */ + +LowlevelUp.prototype.dump = co(function* dump() { + var records = {}; + var i, items, item, key, value; + + items = yield this.range({ + gte: new Buffer([0x00]), + lte: new Buffer([0xff]) + }); + + for (i = 0; i < items.length; i++) { + item = items[i]; + key = item.key.toString('hex'); + value = item.value.toString('hex'); + records[key] = value; + } + + return records; +}); /** * Write and assert a version number for the database. * @param {Number} version - * @param {Function} callback + * @returns {Promise} */ -LowlevelUp.prototype.checkVersion = function checkVersion(key, version, callback) { - var self = this; - this.get(key, function(err, data) { - if (err) - return callback(err); +LowlevelUp.prototype.checkVersion = co(function* checkVersion(key, version) { + var data = yield this.get(key); - if (!data) { - data = new Buffer(4); - data.writeUInt32LE(version, 0, true); - return self.put(key, data, callback); - } + if (!data) { + data = new Buffer(4); + data.writeUInt32LE(version, 0, true); + yield this.put(key, data); + return; + } - data = data.readUInt32LE(0, true); + data = data.readUInt32LE(0, true); - if (data !== version) - return callback(new Error(VERSION_ERROR)); - - callback(); - }); -}; + if (data !== version) + throw new Error(VERSION_ERROR); +}); /** * Clone the database. * @param {String} path - * @param {Function} callback + * @returns {Promise} */ -LowlevelUp.prototype.clone = function clone(path, callback) { - var self = this; - var iter = { keys: true, values: true }; +LowlevelUp.prototype.clone = co(function* clone(path) { var options = utils.merge({}, this.options); + var opt = { keys: true, values: true }; var hwm = 256 << 20; var total = 0; - var tmp, batch; + var tmp, batch, iter, item; assert(!this.loading); assert(!this.closing); @@ -423,50 +485,159 @@ LowlevelUp.prototype.clone = function clone(path, callback) { tmp = new LowlevelUp(path, options); - function done(err) { - tmp.close(function(e) { - if (e) - return callback(e); - callback(err); - }); + yield tmp.open(); + + batch = tmp.batch(); + iter = this.iterator(opt); + + for (;;) { + item = yield iter.next(); + + if (!item) + break; + + batch.put(item.key, item.value); + total += item.value.length; + + if (total >= hwm) { + total = 0; + try { + yield batch.write(); + } catch (e) { + yield iter.end(); + yield tmp.close(); + throw e; + } + batch = tmp.batch(); + } } - tmp.open(function(err) { - if (err) - return callback(err); + try { + yield batch.write(); + } finally { + yield tmp.close(); + } +}); - batch = tmp.batch(); +/** + * Batch + * @constructor + * @param {LowlevelUp} db + */ - self.each(iter, function(key, value, next) { - batch.put(key, value); +function Batch(db) { + this.batch = db.binding.batch(); +} - total += value.length; +/** + * Write a value to the batch. + * @param {String|Buffer} key + * @param {Buffer} value + */ - if (total >= hwm) { - total = 0; - batch.write(function(err) { - if (err) - return next(err); - batch = tmp.batch(); - next(); +Batch.prototype.put = function(key, value) { + this.batch.put(key, value); + return this; +}; + +/** + * Delete a value from the batch. + * @param {String|Buffer} key + */ + +Batch.prototype.del = function del(key) { + this.batch.del(key); + return this; +}; + +/** + * Write batch to database. + * @returns {Promise} + */ + +Batch.prototype.write = function write() { + var self = this; + return new Promise(function(resolve, reject) { + self.batch.write(co.wrap(resolve, reject)); + }); +}; + +/** + * Clear the batch. + */ + +Batch.prototype.clear = function clear() { + this.batch.clear(); + return this; +}; + +/** + * Iterator + * @constructor + * @param {LowlevelUp} db + * @param {Object} options + */ + +function Iterator(db, options) { + this.iter = db.db.iterator(options); +} + +/** + * Seek to the next key. + * @returns {Promise} + */ + +Iterator.prototype.next = function() { + var self = this; + return new Promise(function(resolve, reject) { + self.iter.next(function(err, key, value) { + if (err) { + self.iter.end(function() { + reject(err); }); return; } - next(); - }, function(err) { - if (err) - return done(err); + if (key === undefined && value === undefined) { + self.iter.end(co.wrap(resolve, reject)); + return; + } - batch.write(done); + resolve(new KeyValue(key, value)); }); }); }; +/** + * Seek to an arbitrary key. + * @param {String|Buffer} + */ + +Iterator.prototype.seek = function seek(key) { + this.iter.seek(key); +}; + +/** + * End the iterator. + * @returns {Promise} + */ + +Iterator.prototype.end = function end() { + var self = this; + return new Promise(function(resolve, reject) { + self.iter.end(co.wrap(resolve, reject)); + }); +}; + /* * Helpers */ +function KeyValue(key, value) { + this.key = key; + this.value = value; +} + function isNotFound(err) { if (!err) return false; diff --git a/lib/db/rbt.js b/lib/db/rbt.js index fe407ebe..646417ca 100644 --- a/lib/db/rbt.js +++ b/lib/db/rbt.js @@ -7,7 +7,7 @@ 'use strict'; var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); var DUMMY = new Buffer([0]); var RED = 0; var BLACK = 1; @@ -15,9 +15,9 @@ var SENTINEL; /** * An iterative red black tree. - * Used for the mempool. Many of its - * options, parameters, and methods - * mimic the leveldown interface. + * Many of its options, parameters, + * and methods mimic the leveldown + * interface. * @exports RBT * @constructor * @param {String?} location - Phony location. @@ -572,7 +572,7 @@ RBT.prototype.range = function range(gte, lte) { /** * Open the database (leveldown method). * @param {Object?} options - * @param {Function} callback + * @returns {Promise} */ RBT.prototype.open = function open(options, callback) { @@ -586,23 +586,23 @@ RBT.prototype.open = function open(options, callback) { this.options = options; - return utils.nextTick(callback); + utils.nextTick(callback); }; /** * Close the database (leveldown method). - * @param {Function} callback + * @returns {Promise} */ RBT.prototype.close = function close(callback) { - return utils.nextTick(callback); + utils.nextTick(callback); }; /** * Retrieve a record (leveldown method). * @param {Buffer|String} key * @param {Object?} options - * @param {Function} callback - Returns [Error, Buffer]. + * @returns {Promise} - Returns Buffer. */ RBT.prototype.get = function get(key, options, callback) { @@ -622,13 +622,18 @@ RBT.prototype.get = function get(key, options, callback) { err = new Error('RBT_NOTFOUND: Key not found.'); err.notFound = true; err.type = 'NotFoundError'; - return utils.asyncify(callback)(err); + utils.nextTick(function() { + callback(err); + }); + return; } if (options.asBuffer === false) value = value.toString('utf8'); - return utils.asyncify(callback)(null, value); + utils.nextTick(function() { + callback(null, value); + }); }; /** @@ -636,7 +641,7 @@ RBT.prototype.get = function get(key, options, callback) { * @param {Buffer|String} key * @param {Buffer} value * @param {Object?} options - * @param {Function} callback + * @returns {Promise} */ RBT.prototype.put = function put(key, value, options, callback) { @@ -647,14 +652,14 @@ RBT.prototype.put = function put(key, value, options, callback) { this.insert(key, value); - return utils.nextTick(callback); + utils.nextTick(callback); }; /** * Remove a record (leveldown method). * @param {Buffer|String} key * @param {Object?} options - * @param {Function} callback + * @returns {Promise} */ RBT.prototype.del = function del(key, options, callback) { @@ -665,7 +670,7 @@ RBT.prototype.del = function del(key, options, callback) { this.remove(key); - return utils.nextTick(callback); + utils.nextTick(callback); }; /** @@ -673,7 +678,7 @@ RBT.prototype.del = function del(key, options, callback) { * @see Leveldown.Batch * @param {Object[]?} ops * @param {Object?} options - * @param {Function} callback + * @returns {Promise} * @returns {Leveldown.Batch} */ @@ -722,7 +727,7 @@ RBT.prototype.getProperty = function getProperty(name) { * Calculate approximate database size (leveldown method). * @param {Buffer|String} start - Start key. * @param {Buffer|String} end - End key. - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ RBT.prototype.approximateSize = function approximateSize(start, end, callback) { @@ -736,27 +741,29 @@ RBT.prototype.approximateSize = function approximateSize(start, end, callback) { size += item.value.length; } - return utils.asyncify(callback)(null, size); + utils.nextTick(function() { + callback(null, size); + }); }; /** * Destroy the database (leveldown function) (NOP). * @param {String} location - * @param {Function} callback + * @returns {Promise} */ RBT.destroy = function destroy(location, callback) { - return utils.nextTick(callback); + utils.nextTick(callback); }; /** * Repair the database (leveldown function) (NOP). * @param {String} location - * @param {Function} callback + * @returns {Promise} */ RBT.repair = function repair(location, callback) { - return utils.nextTick(callback); + utils.nextTick(callback); }; /** @@ -934,23 +941,34 @@ Batch.prototype.del = function del(key) { /** * Commit the batch. - * @param {Function} callback + * @returns {Promise} */ Batch.prototype.write = function write(callback) { var i, op; - if (!this.tree) - return callback(new Error('Already written.')); + if (!this.tree) { + utils.nextTick(function() { + callback(new Error('Already written.')); + }); + return; + } for (i = 0; i < this.ops.length; i++) { op = this.ops[i]; - if (op.type === 'put') - this.tree.insert(op.key, op.value); - else if (op.type === 'del') - this.tree.remove(op.key); - else - assert(false); + switch (op.type) { + case 'put': + this.tree.insert(op.key, op.value); + break; + case 'del': + this.tree.remove(op.key); + break; + default: + utils.nextTick(function() { + callback(new Error('Bad operation: ' + op.type)); + }); + return; + } } this.ops.length = 0; @@ -1023,14 +1041,18 @@ function Iterator(tree, options) { /** * Seek to the next key. - * @param {Function} callback + * @returns {Promise} */ Iterator.prototype.next = function(callback) { var item, key, value; - if (this.ended) - return utils.asyncify(callback)(new Error('Cannot call next after end.')); + if (this.ended) { + utils.nextTick(function() { + callback(new Error('Cannot call next after end.')); + }); + return; + } if (this.options.reverse) item = this.snapshot[this.index--]; @@ -1040,13 +1062,15 @@ Iterator.prototype.next = function(callback) { if (this.options.limit != null) { if (this.total++ >= this.options.limit) { this._end(); - return utils.nextTick(callback); + utils.nextTick(callback); + return; } } if (!item) { this._end(); - return utils.nextTick(callback); + utils.nextTick(callback); + return; } key = item.key; @@ -1064,7 +1088,9 @@ Iterator.prototype.next = function(callback) { if (this.options.valueAsBuffer === false) value = value.toString('utf8'); - utils.asyncify(callback)(null, key, value); + utils.nextTick(function() { + callback(null, key, value); + }); }; /** @@ -1105,13 +1131,17 @@ Iterator.prototype._end = function end() { */ Iterator.prototype.end = function end(callback) { - if (this.ended) - return utils.asyncify(callback)(new Error('Already ended.')); + if (this.ended) { + utils.nextTick(function() { + callback(new Error('Already ended.')); + }); + return; + } this.ended = true; this._end(); - return utils.nextTick(callback); + utils.nextTick(callback); }; /* diff --git a/lib/env.js b/lib/env.js index 0b3ba86c..c7ab35a4 100644 --- a/lib/env.js +++ b/lib/env.js @@ -7,9 +7,6 @@ 'use strict'; -var utils = require('./utils/utils'); -var global = utils.global; - /** * A BCoin "environment" which is used for * bootstrapping the initial `bcoin` module. @@ -126,13 +123,11 @@ function Environment() { this.require('bloom', './utils/bloom'); this.require('uri', './utils/uri'); this.require('errors', './utils/errors'); + this.require('co', './utils/co'); // Crypto this.require('ec', './crypto/ec'); this.require('crypto', './crypto/crypto'); - this.require('chachapoly', './crypto/chachapoly'); - this.require('scrypt', './crypto/scrypt'); - this.require('siphash', './crypto/siphash'); // DB this.require('lowlevelup', './db/lowlevelup'); @@ -145,7 +140,6 @@ function Environment() { this.require('stack', './script/stack'); this.require('witness', './script/witness'); this.require('program', './script/program'); - this.require('sc', './script/sigcache'); // Primitives this.require('address', './primitives/address'); @@ -156,8 +150,6 @@ function Environment() { this.require('invitem', './primitives/invitem'); this.require('tx', './primitives/tx'); this.require('mtx', './primitives/mtx'); - this.require('abstractblock', './primitives/abstractblock'); - this.require('memblock', './primitives/memblock'); this.require('block', './primitives/block'); this.require('merkleblock', './primitives/merkleblock'); this.require('headers', './primitives/headers'); @@ -175,7 +167,7 @@ function Environment() { this.require('fullnode', './node/fullnode'); // Net - this.require('timedata', './net/timedata'); + this.require('time', './net/timedata'); this.require('packets', './net/packets'); this.require('bip150', './net/bip150'); this.require('bip151', './net/bip151'); @@ -184,8 +176,6 @@ function Environment() { this.require('pool', './net/pool'); // Chain - this.require('coins', './chain/coins'); - this.require('coinview', './chain/coinview'); this.require('chainentry', './chain/chainentry'); this.require('chaindb', './chain/chaindb'); this.require('chain', './chain/chain'); @@ -193,7 +183,7 @@ function Environment() { // Mempool this.require('fees', './mempool/fees'); this.require('mempool', './mempool/mempool'); - this.expose('mempoolentry', 'mempool', 'MempoolEntry'); + this.require('mempoolentry', './mempool/mempoolentry'); // Miner this.require('miner', './miner/miner'); @@ -204,34 +194,18 @@ function Environment() { this.require('account', './wallet/account'); this.require('walletdb', './wallet/walletdb'); this.require('path', './wallet/path'); + this.require('masterkey', './wallet/masterkey'); + this.require('walletkey', './wallet/walletkey'); // HTTP this.require('http', './http'); + this.require('rpc', './http/rpc'); // Workers this.require('workers', './workers/workers'); // Horrible BIP - this.require('bip70', './bip70/bip70'); - - // Global Instances - this.instance('sigcache', 'sc', 0); - this.instance('time', 'timedata'); - this.instance('defaultLogger', 'logger', 'none'); - this.instance('workerPool', 'workers'); - - // Global Worker Properties - this.useWorkers = false; - this.master = null; - - // Initialize the environment. - this.set({ - network: process.env.BCOIN_NETWORK || 'main', - useWorkers: +process.env.BCOIN_USE_WORKERS === 1, - maxWorkers: +process.env.BCOIN_MAX_WORKERS, - workerTimeout: +process.env.BCOIN_WORKER_TIMEOUT, - sigcacheSize: +process.env.BCOIN_SIGCACHE_SIZE - }); + this.require('bip70', './bip70'); } /** @@ -249,38 +223,6 @@ Environment.prototype.require = function _require(key, path) { }); }; -/** - * Assign a property for a lazily required module. - * @param {String} key - * @param {String} object - * @param {String} property - */ - -Environment.prototype.expose = function expose(key, object, property) { - var cache; - this.__defineGetter__(key, function() { - if (!cache) - cache = this[object][property]; - return cache; - }); -}; - -/** - * Assign an object instance for a lazily assigned property. - * @param {String} key - * @param {String} object - * @param {String} property - */ - -Environment.prototype.instance = function instance(key, object, arg) { - var cache; - this.__defineGetter__(key, function() { - if (!cache) - cache = new this[object](arg); - return cache; - }); -}; - /** * Set the default network. * @param {String} options @@ -296,21 +238,9 @@ Environment.prototype.set = function set(options) { if (options.network) this.network.set(options.network); - if (typeof options.useWorkers === 'boolean') - this.useWorkers = options.useWorkers; + this.workers.set(options); - if (utils.isNumber(options.maxWorkers)) - this.workerPool.size = options.maxWorkers; - - if (utils.isNumber(options.workerTimeout)) - this.workerPool.timeout = options.workerTimeout; - - if (utils.isBrowser && this.useWorkers) { - this.useWorkers = typeof global.Worker === 'function' - || typeof global.postMessage === 'function'; - } - - if (utils.isNumber(options.sigcacheSize)) + if (options.sigcacheSize != null) this.sigcache.resize(options.sigcacheSize); return this; @@ -327,80 +257,15 @@ Environment.prototype.now = function now() { /** * Cache all necessary modules. - * Used for benchmarks and browserify. */ Environment.prototype.cache = function cache() { - require('bn.js'); - require('./protocol/constants'); - require('./protocol/networks'); - require('./protocol/network'); - require('./utils/utils'); - require('./utils/locker'); - require('./utils/reader'); - require('./utils/writer'); - require('./utils/lru'); - require('./utils/bloom'); - require('./utils/uri'); - require('./utils/errors'); - require('./crypto/ec'); - require('./crypto/crypto'); - require('./crypto/chachapoly'); - require('./crypto/scrypt'); - require('./crypto/siphash'); - require('./db/lowlevelup'); - require('./db/ldb'); - require('./db/rbt'); - require('./script/script'); - require('./script/opcode'); - require('./script/stack'); - require('./script/witness'); - require('./script/program'); - require('./script/sigcache'); - require('./primitives/address'); - require('./primitives/outpoint'); - require('./primitives/input'); - require('./primitives/output'); - require('./primitives/coin'); - require('./primitives/invitem'); - require('./primitives/tx'); - require('./primitives/mtx'); - require('./primitives/abstractblock'); - require('./primitives/memblock'); - require('./primitives/block'); - require('./primitives/merkleblock'); - require('./primitives/headers'); - require('./primitives/keyring'); - require('./primitives/netaddress'); - require('./hd/hd'); - require('./node/logger'); - require('./node/config'); - require('./node/node'); - require('./node/spvnode'); require('./node/fullnode'); - require('./net/timedata'); - require('./net/packets'); - require('./net/bip150'); - require('./net/bip151'); - require('./net/bip152'); - require('./net/peer'); - require('./net/pool'); - require('./chain/coins'); - require('./chain/coinview'); - require('./chain/chainentry'); - require('./chain/chaindb'); - require('./chain/chain'); - require('./mempool/fees'); - require('./mempool/mempool'); - require('./miner/miner'); - require('./miner/minerblock'); - require('./wallet/wallet'); - require('./wallet/account'); - require('./wallet/walletdb'); - require('./wallet/path'); + require('./node/spvnode'); require('./http'); - require('./workers/workers'); - require('./bip70/bip70'); + require('./crypto/schnorr'); + require('./utils/uri'); + require('./bip70'); }; /* @@ -409,12 +274,8 @@ Environment.prototype.cache = function cache() { */ exports.require = Environment.prototype.require; -exports.expose = Environment.prototype.expose; -exports.instance = Environment.prototype.instance; exports.cache = Environment.prototype.cache; exports.set = Environment.prototype.set; exports.now = Environment.prototype.now; Environment.call(exports); - -utils.fastProp(exports); diff --git a/lib/hd/hd.js b/lib/hd/hd.js index 2314e175..3b1c2ae8 100644 --- a/lib/hd/hd.js +++ b/lib/hd/hd.js @@ -6,10 +6,8 @@ 'use strict'; -var bcoin = require('../env'); -var utils = require('../utils/utils'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); var LRU = require('../utils/lru'); var Mnemonic = require('./mnemonic'); var HDPrivateKey = require('./private'); @@ -114,26 +112,21 @@ HD.fromExtended = function fromExtended(data) { */ HD.from = function from(options, network) { - var xkey; - assert(options, 'Options required.'); - if (options.xkey) - xkey = options.xkey; - else if (options.xpubkey) - xkey = options.xpubkey; - else if (options.xprivkey) - xkey = options.xprivkey; - else - xkey = options; + if (HD.isHD(options)) + return options; - if (HD.isExtended(xkey)) - return HD.fromBase58(xkey); + if (HD.isExtended(options)) + return HD.fromBase58(options); if (HD.hasPrefix(options)) return HD.fromRaw(options); - return HD.fromMnemonic(options, network); + if (options && typeof options === 'object') + return HD.fromMnemonic(options, network); + + throw new Error('Cannot create HD key from bad options.'); }; /** diff --git a/lib/hd/index.js b/lib/hd/index.js new file mode 100644 index 00000000..02675b8e --- /dev/null +++ b/lib/hd/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./hd'); diff --git a/lib/hd/mnemonic.js b/lib/hd/mnemonic.js index 2573d3bc..165a223c 100644 --- a/lib/hd/mnemonic.js +++ b/lib/hd/mnemonic.js @@ -6,11 +6,10 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); var BufferWriter = require('../utils/writer'); var BufferReader = require('../utils/reader'); var HD = require('./hd'); diff --git a/lib/hd/private.js b/lib/hd/private.js index 12667035..d3a6a605 100644 --- a/lib/hd/private.js +++ b/lib/hd/private.js @@ -6,13 +6,13 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); var ec = require('../crypto/ec'); -var assert = utils.assert; -var constants = bcoin.constants; -var networks = bcoin.networks; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var networks = require('../protocol/networks'); +var Network = require('../protocol/network'); var BufferWriter = require('../utils/writer'); var BufferReader = require('../utils/reader'); var HD = require('./hd'); @@ -52,7 +52,7 @@ function HDPrivateKey(options) { if (!(this instanceof HDPrivateKey)) return new HDPrivateKey(options); - this.network = bcoin.network.get(); + this.network = Network.primary; this.depth = 0; this.parentFingerPrint = FINGER_PRINT; this.childIndex = 0; @@ -89,7 +89,7 @@ HDPrivateKey.prototype.fromOptions = function fromOptions(options) { assert(options.depth <= 0xff, 'Depth is too high.'); if (options.network) - this.network = bcoin.network.get(options.network); + this.network = Network.get(options.network); this.depth = options.depth; this.parentFingerPrint = options.parentFingerPrint; @@ -187,11 +187,19 @@ HDPrivateKey.prototype.destroy = function destroy(pub) { * @returns {HDPrivateKey} */ -HDPrivateKey.prototype.derive = function derive(index, hardened) { +HDPrivateKey.prototype.derive = function derive(index, hardened, cache) { var p, id, data, hash, left, right, key, child; + if (typeof hardened !== 'boolean') { + cache = hardened; + hardened = false; + } + + if (!cache) + cache = HD.cache; + if (typeof index === 'string') - return this.derivePath(index); + return this.derivePath(index, cache); hardened = index >= constants.hd.HARDENED ? true : hardened; @@ -204,11 +212,12 @@ HDPrivateKey.prototype.derive = function derive(index, hardened) { if (this.depth >= 0xff) throw new Error('Depth too high.'); - id = this.getID(index); - child = HD.cache.get(id); - - if (child) - return child; + if (cache) { + id = this.getID(index); + child = cache.get(id); + if (child) + return child; + } p = new BufferWriter(); @@ -230,7 +239,7 @@ HDPrivateKey.prototype.derive = function derive(index, hardened) { try { key = ec.privateKeyTweakAdd(this.privateKey, left); } catch (e) { - return this.derive(index + 1); + return this.derive(index + 1, cache); } if (!this.fingerPrint) @@ -245,7 +254,8 @@ HDPrivateKey.prototype.derive = function derive(index, hardened) { child.privateKey = key; child.publicKey = ec.publicKeyCreate(key, true); - HD.cache.set(id, child); + if (cache) + cache.set(id, child); return child; }; @@ -270,13 +280,13 @@ HDPrivateKey.prototype.getID = function getID(index) { * @throws Error if key is not a master key. */ -HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(accountIndex) { +HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(accountIndex, cache) { assert(utils.isNumber(accountIndex), 'Account index must be a number.'); assert(this.isMaster(), 'Cannot derive account index.'); return this - .derive(44, true) - .derive(this.network.keyPrefix.coinType, true) - .derive(accountIndex, true); + .derive(44, true, cache) + .derive(this.network.keyPrefix.coinType, true, cache) + .derive(accountIndex, true, cache); }; /** @@ -284,9 +294,9 @@ HDPrivateKey.prototype.deriveAccount44 = function deriveAccount44(accountIndex) * @returns {HDPrivateKey} */ -HDPrivateKey.prototype.derivePurpose45 = function derivePurpose45() { +HDPrivateKey.prototype.derivePurpose45 = function derivePurpose45(cache) { assert(this.isMaster(), 'Cannot derive purpose 45.'); - return this.derive(45, true); + return this.derive(45, true, cache); }; /** @@ -395,13 +405,13 @@ HDPrivateKey.isValidPath = function isValidPath(path) { * @throws Error if `path` is not a valid path. */ -HDPrivateKey.prototype.derivePath = function derivePath(path) { +HDPrivateKey.prototype.derivePath = function derivePath(path, cache) { var indexes = HD.parsePath(path, constants.hd.MAX_INDEX); var key = this; var i; for (i = 0; i < indexes.length; i++) - key = key.derive(indexes[i]); + key = key.derive(indexes[i], cache); return key; }; @@ -490,7 +500,7 @@ HDPrivateKey.prototype.fromSeed = function fromSeed(seed, network) { if (!ec.privateKeyVerify(left)) throw new Error('Master private key is invalid.'); - this.network = bcoin.network.get(network); + this.network = Network.get(network); this.depth = 0; this.parentFingerPrint = new Buffer([0, 0, 0, 0]); this.childIndex = 0; @@ -549,7 +559,7 @@ HDPrivateKey.fromMnemonic = function fromMnemonic(mnemonic, network) { HDPrivateKey.prototype.fromKey = function fromKey(key, entropy, network) { assert(Buffer.isBuffer(key) && key.length === 32); assert(Buffer.isBuffer(entropy) && entropy.length === 32); - this.network = bcoin.network.get(network); + this.network = Network.get(network); this.depth = 0; this.parentFingerPrint = new Buffer([0, 0, 0, 0]); this.childIndex = 0; @@ -624,7 +634,7 @@ HDPrivateKey.prototype.fromRaw = function fromRaw(raw) { assert(i < networks.types.length, 'Network not found.'); this.publicKey = ec.publicKeyCreate(this.privateKey, true); - this.network = bcoin.network.get(type); + this.network = Network.get(type); return this; }; @@ -651,7 +661,7 @@ HDPrivateKey.prototype.toRaw = function toRaw(network, writer) { if (!network) network = this.network; - network = bcoin.network.get(network); + network = Network.get(network); p.writeU32BE(network.keyPrefix.xprivkey); p.writeU8(this.depth); diff --git a/lib/hd/public.js b/lib/hd/public.js index a03fd59f..16d175f8 100644 --- a/lib/hd/public.js +++ b/lib/hd/public.js @@ -6,13 +6,13 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); var ec = require('../crypto/ec'); -var assert = utils.assert; -var constants = bcoin.constants; -var networks = bcoin.networks; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var networks = require('../protocol/networks'); +var Network = require('../protocol/network'); var BufferWriter = require('../utils/writer'); var BufferReader = require('../utils/reader'); var HD = require('./hd'); @@ -47,7 +47,7 @@ function HDPublicKey(options) { if (!(this instanceof HDPublicKey)) return new HDPublicKey(options); - this.network = bcoin.network.get(); + this.network = Network.primary; this.depth = 0; this.parentFingerPrint = FINGER_PRINT; this.childIndex = 0; @@ -80,7 +80,7 @@ HDPublicKey.prototype.fromOptions = function fromOptions(options) { assert(Buffer.isBuffer(options.publicKey)); if (options.network) - this.network = bcoin.network.get(options.network); + this.network = Network.get(options.network); this.depth = options.depth; this.parentFingerPrint = options.parentFingerPrint; @@ -140,11 +140,19 @@ HDPublicKey.prototype.destroy = function destroy() { * @throws on `hardened` */ -HDPublicKey.prototype.derive = function derive(index, hardened) { +HDPublicKey.prototype.derive = function derive(index, hardened, cache) { var p, id, data, hash, left, right, key, child; + if (typeof hardened !== 'boolean') { + cache = hardened; + hardened = false; + } + + if (!cache) + cache = HD.cache; + if (typeof index === 'string') - return this.derivePath(index); + return this.derivePath(index, cache); if (index >= constants.hd.HARDENED || hardened) throw new Error('Index out of range.'); @@ -155,11 +163,12 @@ HDPublicKey.prototype.derive = function derive(index, hardened) { if (this.depth >= 0xff) throw new Error('Depth too high.'); - id = this.getID(index); - child = HD.cache.get(id); - - if (child) - return child; + if (cache) { + id = this.getID(index); + child = cache.get(id); + if (child) + return child; + } p = new BufferWriter(); p.writeBytes(this.publicKey); @@ -173,7 +182,7 @@ HDPublicKey.prototype.derive = function derive(index, hardened) { try { key = ec.publicKeyTweakAdd(this.publicKey, left, true); } catch (e) { - return this.derive(index + 1); + return this.derive(index + 1, cache); } if (!this.fingerPrint) @@ -187,7 +196,8 @@ HDPublicKey.prototype.derive = function derive(index, hardened) { child.chainCode = right; child.publicKey = key; - HD.cache.set(id, child); + if (cache) + cache.set(id, child); return child; }; @@ -288,13 +298,13 @@ HDPublicKey.isValidPath = function isValidPath(path) { * @throws Error if hardened. */ -HDPublicKey.prototype.derivePath = function derivePath(path) { +HDPublicKey.prototype.derivePath = function derivePath(path, cache) { var indexes = HD.parsePath(path, constants.hd.HARDENED); var key = this; var i; for (i = 0; i < indexes.length; i++) - key = key.derive(indexes[i]); + key = key.derive(indexes[i], cache); return key; }; @@ -475,7 +485,7 @@ HDPublicKey.prototype.fromRaw = function fromRaw(raw) { assert(i < networks.types.length, 'Network not found.'); - this.network = bcoin.network.get(type); + this.network = Network.get(type); return this; }; @@ -502,7 +512,7 @@ HDPublicKey.prototype.toRaw = function toRaw(network, writer) { if (!network) network = this.network; - network = bcoin.network.get(network); + network = Network.get(network); p.writeU32BE(network.keyPrefix.xpubkey); p.writeU8(this.depth); diff --git a/lib/http/base.js b/lib/http/base.js index 1aaadd7d..beb2b345 100644 --- a/lib/http/base.js +++ b/lib/http/base.js @@ -9,7 +9,7 @@ var AsyncObject = require('../utils/async'); var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); /** * HTTPBase @@ -162,7 +162,7 @@ HTTPBase.prototype._initRouter = function _initRouter() { // Avoid stack overflows utils.nextTick(function() { try { - callback(req, res, next, _send); + callback.call(route.ctx, req, res, _send, next); } catch (e) { done(e); } @@ -208,28 +208,36 @@ HTTPBase.prototype._initIO = function _initIO() { /** * Open the server. * @alias HTTPBase#open - * @param {Function} callback + * @returns {Promise} */ -HTTPBase.prototype._open = function open(callback) { +HTTPBase.prototype._open = function open() { assert(typeof this.options.port === 'number', 'Port required.'); - this.listen(this.options.port, this.options.host, callback); + return this.listen(this.options.port, this.options.host); }; /** * Close the server. * @alias HTTPBase#close - * @param {Function} callback + * @returns {Promise} */ HTTPBase.prototype._close = function close(callback) { - if (this.io) { - this.server.once('close', callback); - this.io.close(); - return; - } + var self = this; - this.server.close(callback); + return new Promise(function(resolve, reject) { + if (self.io) { + self.server.once('close', resolve); + self.io.close(); + return; + } + + self.server.close(function(err) { + if (err) + return reject(err); + resolve(); + }); + }); }; /** @@ -237,7 +245,7 @@ HTTPBase.prototype._close = function close(callback) { * @param {HTTPRequest} req * @param {HTTPResponse} res * @param {Function} _send - * @param {Function} callback + * @returns {Promise} * @private */ @@ -256,11 +264,11 @@ HTTPBase.prototype._handle = function _handle(req, res, _send, callback) { handler = self.stack[i++]; utils.nextTick(function() { - if (handler.path && req.pathname.indexOf(handler.path) === -1) + if (handler.path && req.pathname.indexOf(handler.path) !== 0) return next(); try { - handler.callback(req, res, next, _send); + handler.callback.call(handler.ctx, req, res, _send, next); } catch (e) { next(e); } @@ -283,7 +291,7 @@ HTTPBase.prototype._handle = function _handle(req, res, _send, callback) { * @param {RouteCallback} callback */ -HTTPBase.prototype.use = function use(path, callback) { +HTTPBase.prototype.use = function use(path, callback, ctx) { if (!callback) { callback = path; path = null; @@ -291,12 +299,12 @@ HTTPBase.prototype.use = function use(path, callback) { if (Array.isArray(path)) { path.forEach(function(path) { - this.use(path, callback); + this.use(path, callback, ctx); }, this); return; } - this.stack.push({ path: path, callback: callback }); + this.stack.push({ ctx: ctx, path: path, callback: callback }); }; /** @@ -305,14 +313,14 @@ HTTPBase.prototype.use = function use(path, callback) { * @param {RouteCallback} callback */ -HTTPBase.prototype.get = function get(path, callback) { +HTTPBase.prototype.get = function get(path, callback, ctx) { if (Array.isArray(path)) { path.forEach(function(path) { - this.get(path, callback); + this.get(path, callback, ctx); }, this); return; } - this.routes.get.push({ path: path, callback: callback }); + this.routes.get.push({ ctx: ctx, path: path, callback: callback }); }; /** @@ -321,14 +329,14 @@ HTTPBase.prototype.get = function get(path, callback) { * @param {RouteCallback} callback */ -HTTPBase.prototype.post = function post(path, callback) { +HTTPBase.prototype.post = function post(path, callback, ctx) { if (Array.isArray(path)) { path.forEach(function(path) { - this.post(path, callback); + this.post(path, callback, ctx); }, this); return; } - this.routes.post.push({ path: path, callback: callback }); + this.routes.post.push({ ctx: ctx, path: path, callback: callback }); }; /** @@ -337,14 +345,14 @@ HTTPBase.prototype.post = function post(path, callback) { * @param {RouteCallback} callback */ -HTTPBase.prototype.put = function put(path, callback) { +HTTPBase.prototype.put = function put(path, callback, ctx) { if (Array.isArray(path)) { path.forEach(function(path) { - this.put(path, callback); + this.put(path, callback, ctx); }, this); return; } - this.routes.put.push({ path: path, callback: callback }); + this.routes.put.push({ ctx: ctx, path: path, callback: callback }); }; /** @@ -353,14 +361,14 @@ HTTPBase.prototype.put = function put(path, callback) { * @param {RouteCallback} callback */ -HTTPBase.prototype.del = function del(path, callback) { +HTTPBase.prototype.del = function del(path, callback, ctx) { if (Array.isArray(path)) { path.forEach(function(path) { - this.del(path, callback); + this.del(path, callback, ctx); }, this); return; } - this.routes.del.push({ path: path, callback: callback }); + this.routes.del.push({ ctx: ctx, path: path, callback: callback }); }; /** @@ -376,26 +384,24 @@ HTTPBase.prototype.address = function address() { * Listen on port and host. * @param {Number} port * @param {String?} host - * @param {Function} callback + * @returns {Promise} */ -HTTPBase.prototype.listen = function listen(port, host, callback) { +HTTPBase.prototype.listen = function listen(port, host) { var self = this; - var addr; + return new Promise(function(resolve, reject) { + var addr; - this.server.listen(port, host, function(err) { - if (err) { - if (callback) - return callback(err); - throw err; - } + self.server.listen(port, host, function(err) { + if (err) + return reject(err); - addr = self.address(); + addr = self.address(); - self.emit('listening', addr); + self.emit('listening', addr); - if (callback) - callback(null, addr); + resolve(addr); + }); }); }; diff --git a/lib/http/client.js b/lib/http/client.js index b8d3e12b..49568050 100644 --- a/lib/http/client.js +++ b/lib/http/client.js @@ -11,8 +11,9 @@ var Network = require('../protocol/network'); var AsyncObject = require('../utils/async'); var RPCClient = require('./rpcclient'); var utils = require('../utils/utils'); -var assert = utils.assert; -var request = require('./request'); +var co = require('../utils/co'); +var assert = require('assert'); +var request = require('./request').promise; /** * BCoin HTTP client. @@ -44,7 +45,7 @@ function HTTPClient(options) { this.rpc = new RPCClient(options); // Open automatically. - this.open(); + // this.open(); } utils.inherits(HTTPClient, AsyncObject); @@ -52,10 +53,10 @@ utils.inherits(HTTPClient, AsyncObject); /** * Open the client, wait for socket to connect. * @alias HTTPClient#open - * @param {Function} callback + * @returns {Promise} */ -HTTPClient.prototype._open = function _open(callback) { +HTTPClient.prototype._open = co(function* _open() { var self = this; var IOClient; @@ -66,7 +67,7 @@ HTTPClient.prototype._open = function _open(callback) { } if (!IOClient) - return callback(); + return; this.socket = new IOClient(this.uri, { transports: ['websocket'], @@ -115,11 +116,24 @@ HTTPClient.prototype._open = function _open(callback) { }); }); - this.socket.on('connect', function() { + yield this._onConnect(); + yield this._sendAuth(); +}); + +HTTPClient.prototype._onConnect = function _onConnect() { + var self = this; + return new Promise(function(resolve, reject) { + self.socket.once('connect', resolve); + }); +}; + +HTTPClient.prototype._sendAuth = function _sendAuth() { + var self = this; + return new Promise(function(resolve, reject) { self.socket.emit('auth', self.apiKey, function(err) { if (err) - return callback(new Error(err.error)); - callback(); + return reject(new Error(err.error)); + resolve(); }); }); }; @@ -127,17 +141,17 @@ HTTPClient.prototype._open = function _open(callback) { /** * Close the client, wait for the socket to close. * @alias HTTPClient#close - * @param {Function} callback + * @returns {Promise} */ -HTTPClient.prototype._close = function close(callback) { +HTTPClient.prototype._close = function close() { if (!this.socket) - return utils.nextTick(callback); + return Promise.resolve(null); this.socket.disconnect(); this.socket = null; - utils.nextTick(callback); + return Promise.resolve(null); }; /** @@ -146,17 +160,11 @@ HTTPClient.prototype._close = function close(callback) { * @param {String} method * @param {String} endpoint - Path. * @param {Object} json - Body or query depending on method. - * @param {Function} callback - Returns [Error, Object?]. + * @returns {Promise} - Returns Object?. */ -HTTPClient.prototype._request = function _request(method, endpoint, json, callback) { - var self = this; - var query, network, height; - - if (!callback) { - callback = json; - json = null; - } +HTTPClient.prototype._request = co(function* _request(method, endpoint, json) { + var query, network, height, res; if (this.token) { if (!json) @@ -169,7 +177,7 @@ HTTPClient.prototype._request = function _request(method, endpoint, json, callba json = null; } - request({ + res = yield request({ method: method, uri: this.uri + endpoint, query: query, @@ -179,50 +187,43 @@ HTTPClient.prototype._request = function _request(method, endpoint, json, callba password: this.apiKey || '' }, expect: 'json' - }, function(err, res, body) { - if (err) - return callback(err); - - network = res.headers['x-bcoin-network']; - - if (network !== self.network.type) - return callback(new Error('Wrong network.')); - - height = +res.headers['x-bcoin-height']; - - if (utils.isNumber(height)) - self.network.updateHeight(height); - - if (res.statusCode === 404) - return callback(); - - if (!body) - return callback(new Error('No body.')); - - if (res.statusCode !== 200) { - if (body.error) - return callback(new Error(body.error)); - return callback(new Error('Status code: ' + res.statusCode)); - } - - try { - return callback(null, body); - } catch (e) { - return callback(e); - } }); -}; + + network = res.headers['x-bcoin-network']; + + if (network !== this.network.type) + throw new Error('Wrong network.'); + + height = +res.headers['x-bcoin-height']; + + if (utils.isNumber(height)) + this.network.updateHeight(height); + + if (res.statusCode === 404) + return; + + if (!res.body) + throw new Error('No body.'); + + if (res.statusCode !== 200) { + if (res.body.error) + throw new Error(res.body.error); + throw new Error('Status code: ' + res.statusCode); + } + + return res.body; +}); /** * Make a GET http request to endpoint. * @private * @param {String} endpoint - Path. * @param {Object} json - Querystring. - * @param {Function} callback - Returns [Error, Object?]. + * @returns {Promise} - Returns Object?. */ -HTTPClient.prototype._get = function _get(endpoint, json, callback) { - this._request('get', endpoint, json, callback); +HTTPClient.prototype._get = function _get(endpoint, json) { + return this._request('get', endpoint, json); }; /** @@ -230,11 +231,11 @@ HTTPClient.prototype._get = function _get(endpoint, json, callback) { * @private * @param {String} endpoint - Path. * @param {Object} json - Body. - * @param {Function} callback - Returns [Error, Object?]. + * @returns {Promise} - Returns Object?. */ -HTTPClient.prototype._post = function _post(endpoint, json, callback) { - this._request('post', endpoint, json, callback); +HTTPClient.prototype._post = function _post(endpoint, json) { + return this._request('post', endpoint, json); }; /** @@ -242,11 +243,11 @@ HTTPClient.prototype._post = function _post(endpoint, json, callback) { * @private * @param {String} endpoint - Path. * @param {Object} json - Body. - * @param {Function} callback - Returns [Error, Object?]. + * @returns {Promise} - Returns Object?. */ -HTTPClient.prototype._put = function _put(endpoint, json, callback) { - this._request('put', endpoint, json, callback); +HTTPClient.prototype._put = function _put(endpoint, json) { + return this._request('put', endpoint, json); }; /** @@ -254,41 +255,41 @@ HTTPClient.prototype._put = function _put(endpoint, json, callback) { * @private * @param {String} endpoint - Path. * @param {Object} json - Body. - * @param {Function} callback - Returns [Error, Object?]. + * @returns {Promise} - Returns Object?. */ -HTTPClient.prototype._del = function _del(endpoint, json, callback) { - this._request('delete', endpoint, json, callback); +HTTPClient.prototype._del = function _del(endpoint, json) { + return this._request('delete', endpoint, json); }; /** * Get a mempool snapshot. - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -HTTPClient.prototype.getMempool = function getMempool(callback) { - this._get('/mempool', callback); +HTTPClient.prototype.getMempool = function getMempool() { + return this._get('/mempool'); }; /** * Get some info about the server (network and version). - * @param {Function} callback - Returns [Error, Object]. + * @returns {Promise} - Returns Object. */ -HTTPClient.prototype.getInfo = function getInfo(callback) { - this._get('/', callback); +HTTPClient.prototype.getInfo = function getInfo() { + return this._get('/'); }; /** * Get coins that pertain to an address from the mempool or chain database. * Takes into account spent coins in the mempool. * @param {Base58Address|Base58Address[]} addresses - * @param {Function} callback - Returns [Error, {@link Coin}[]]. + * @returns {Promise} - Returns {@link Coin}[]. */ -HTTPClient.prototype.getCoinsByAddress = function getCoinsByAddress(address, callback) { +HTTPClient.prototype.getCoinsByAddress = function getCoinsByAddress(address) { var body = { address: address }; - this._post('/coin/address', body, callback); + return this._post('/coin/address', body); }; /** @@ -296,56 +297,55 @@ HTTPClient.prototype.getCoinsByAddress = function getCoinsByAddress(address, cal * Takes into account spent coins in the mempool. * @param {Hash} hash * @param {Number} index - * @param {Function} callback - Returns [Error, {@link Coin}]. + * @returns {Promise} - Returns {@link Coin}. */ -HTTPClient.prototype.getCoin = function getCoin(hash, index, callback) { - this._get('/coin/' + hash + '/' + index, callback); +HTTPClient.prototype.getCoin = function getCoin(hash, index) { + return this._get('/coin/' + hash + '/' + index); }; /** * Retrieve transactions pertaining to an * address from the mempool or chain database. * @param {Base58Address|Base58Address[]} addresses - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -HTTPClient.prototype.getTXByAddress = function getTXByAddress(address, callback) { +HTTPClient.prototype.getTXByAddress = function getTXByAddress(address) { var body = { address: address }; - - this._post('/tx/address', body, callback); + return this._post('/tx/address', body); }; /** * Retrieve a transaction from the mempool or chain database. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -HTTPClient.prototype.getTX = function getTX(hash, callback) { - this._get('/tx/' + hash, callback); +HTTPClient.prototype.getTX = function getTX(hash) { + return this._get('/tx/' + hash); }; /** * Retrieve a block from the chain database. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -HTTPClient.prototype.getBlock = function getBlock(hash, callback) { - this._get('/block/' + hash, callback); +HTTPClient.prototype.getBlock = function getBlock(hash) { + return this._get('/block/' + hash); }; /** * Add a transaction to the mempool and broadcast it. * @param {TX} tx - * @param {Function} callback + * @returns {Promise} */ -HTTPClient.prototype.broadcast = function broadcast(tx, callback) { +HTTPClient.prototype.broadcast = function broadcast(tx) { var body = { tx: toHex(tx) }; - this._post('/broadcast', body, callback); + return this._post('/broadcast', body); }; /** @@ -353,11 +353,19 @@ HTTPClient.prototype.broadcast = function broadcast(tx, callback) { * @param {WalletID} id */ -HTTPClient.prototype.join = function join(id, token, callback) { - if (!this.socket) - return callback(); +HTTPClient.prototype.join = function join(id, token) { + var self = this; - this.socket.emit('wallet join', id, token, callback); + if (!this.socket) + return Promise.resolve(null); + + return new Promise(function(resolve, reject) { + self.socket.emit('wallet join', id, token, function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); }; /** @@ -365,145 +373,113 @@ HTTPClient.prototype.join = function join(id, token, callback) { * @param {WalletID} id */ -HTTPClient.prototype.leave = function leave(id, callback) { - if (!this.socket) - return callback(); +HTTPClient.prototype.leave = function leave(id) { + var self = this; - this.socket.emit('wallet leave', id, callback); + if (!this.socket) + return Promise.resolve(null); + + return new Promise(function(resolve, reject) { + self.socket.emit('wallet leave', id, function(err) { + if (err) + return reject(new Error(err.error)); + resolve(); + }); + }); }; /** * Listen for events on all wallets. */ -HTTPClient.prototype.all = function all(token, callback) { - this.join('!all', token, callback); +HTTPClient.prototype.all = function all(token) { + return this.join('!all', token); }; /** * Unlisten for events on all wallets. */ -HTTPClient.prototype.none = function none(callback) { - this.leave('!all', callback); +HTTPClient.prototype.none = function none() { + return this.leave('!all'); }; /** * Request the raw wallet JSON (will create wallet if it does not exist). * @private * @param {Object} options - See {@link Wallet}. - * @param {Function} callback - Returns [Error, Object]. + * @returns {Promise} - Returns Object. */ -HTTPClient.prototype.createWallet = function createWallet(options, callback) { - this._post('/wallet', options, callback); +HTTPClient.prototype.createWallet = function createWallet(options) { + return this._post('/wallet', options); }; /** * Get the raw wallet JSON. * @private * @param {WalletID} id - * @param {Function} callback - Returns [Error, Object]. + * @returns {Promise} - Returns Object. */ -HTTPClient.prototype.getWallet = function getWallet(id, callback) { - this._get('/wallet/' + id, callback); +HTTPClient.prototype.getWallet = function getWallet(id) { + return this._get('/wallet/' + id); }; /** * Get wallet transaction history. * @param {WalletID} id - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -HTTPClient.prototype.getHistory = function getHistory(id, account, callback) { - var options; - - if (typeof account === 'function') { - callback = account; - account = null; - } - - options = { account: account }; - - this._get('/wallet/' + id + '/tx/history', options, callback); +HTTPClient.prototype.getHistory = function getHistory(id, account) { + var options = { account: account }; + return this._get('/wallet/' + id + '/tx/history', options); }; /** * Get wallet coins. * @param {WalletID} id - * @param {Function} callback - Returns [Error, {@link Coin}[]]. + * @returns {Promise} - Returns {@link Coin}[]. */ -HTTPClient.prototype.getCoins = function getCoins(id, account, callback) { - var options; - - if (typeof account === 'function') { - callback = account; - account = null; - } - - options = { account: account }; - - this._get('/wallet/' + id + '/coin', options, callback); +HTTPClient.prototype.getCoins = function getCoins(id, account) { + var options = { account: account }; + return this._get('/wallet/' + id + '/coin', options); }; /** * Get all unconfirmed transactions. * @param {WalletID} id - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -HTTPClient.prototype.getUnconfirmed = function getUnconfirmed(id, account, callback) { - var options; - - if (typeof account === 'function') { - callback = account; - account = null; - } - - options = { account: account }; - - this._get('/wallet/' + id + '/tx/unconfirmed', options, callback); +HTTPClient.prototype.getUnconfirmed = function getUnconfirmed(id, account) { + var options = { account: account }; + return this._get('/wallet/' + id + '/tx/unconfirmed', options); }; /** * Calculate wallet balance. * @param {WalletID} id - * @param {Function} callback - Returns [Error, {@link Balance}]. + * @returns {Promise} - Returns {@link Balance}. */ -HTTPClient.prototype.getBalance = function getBalance(id, account, callback) { - var options; - - if (typeof account === 'function') { - callback = account; - account = null; - } - - options = { account: account }; - - this._get('/wallet/' + id + '/balance', options, callback); +HTTPClient.prototype.getBalance = function getBalance(id, account) { + var options = { account: account }; + return this._get('/wallet/' + id + '/balance', options); }; /** * Get last N wallet transactions. * @param {WalletID} id * @param {Number} limit - Max number of transactions. - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -HTTPClient.prototype.getLast = function getLast(id, account, limit, callback) { - var options; - - if (typeof account === 'function') { - callback = account; - account = null; - } - - options = { account: account, limit: limit }; - - this._get('/wallet/' + id + '/tx/last', options, callback); +HTTPClient.prototype.getLast = function getLast(id, account, limit) { + var options = { account: account, limit: limit }; + return this._get('/wallet/' + id + '/tx/last', options); }; /** @@ -514,16 +490,10 @@ HTTPClient.prototype.getLast = function getLast(id, account, limit, callback) { * @param {Number} options.end - End time. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -HTTPClient.prototype.getRange = function getRange(id, account, options, callback) { - if (typeof options === 'function') { - callback = options; - options = account; - account = null; - } - +HTTPClient.prototype.getRange = function getRange(id, account, options) { options = { account: account, start: options.start, @@ -531,8 +501,7 @@ HTTPClient.prototype.getRange = function getRange(id, account, options, callback limit: options.limit, reverse: options.reverse }; - - this._get('/wallet/' + id + '/tx/range', options, callback); + return this._get('/wallet/' + id + '/tx/range', options); }; /** @@ -540,21 +509,12 @@ HTTPClient.prototype.getRange = function getRange(id, account, options, callback * is available in the wallet history). * @param {WalletID} id * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -HTTPClient.prototype.getWalletTX = function getWalletTX(id, account, hash, callback) { - var options; - - if (typeof hash === 'function') { - callback = hash; - hash = account; - account = null; - } - - options = { account: account }; - - this._get('/wallet/' + id + '/tx/' + hash, options, callback); +HTTPClient.prototype.getWalletTX = function getWalletTX(id, account, hash) { + var options = { account: account }; + return this._get('/wallet/' + id + '/tx/' + hash, options); }; /** @@ -563,24 +523,13 @@ HTTPClient.prototype.getWalletTX = function getWalletTX(id, account, hash, callb * @param {WalletID} id * @param {Hash} hash * @param {Number} index - * @param {Function} callback - Returns [Error, {@link Coin}[]]. + * @returns {Promise} - Returns {@link Coin}[]. */ -HTTPClient.prototype.getWalletCoin = function getWalletCoin(id, account, hash, index, callback) { - var options, path; - - if (typeof hash === 'function') { - callback = index; - index = hash; - hash = account; - account = null; - } - - options = { account: account }; - - path = '/wallet/' + id + '/coin/' + hash + '/' + index; - - this._get(path, options, callback); +HTTPClient.prototype.getWalletCoin = function getWalletCoin(id, account, hash, index) { + var path = '/wallet/' + id + '/coin/' + hash + '/' + index; + var options = { account: account }; + return this._get(path, options); }; /** @@ -589,10 +538,10 @@ HTTPClient.prototype.getWalletCoin = function getWalletCoin(id, account, hash, i * @param {Object} options * @param {Base58Address} options.address * @param {Amount} options.value - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -HTTPClient.prototype.send = function send(id, options, callback) { +HTTPClient.prototype.send = function send(id, options) { options = utils.merge({}, options); options.outputs = options.outputs || []; @@ -607,54 +556,41 @@ HTTPClient.prototype.send = function send(id, options, callback) { }; }); - this._post('/wallet/' + id + '/send', options, callback); + return this._post('/wallet/' + id + '/send', options); }; /** * Generate a new token. * @param {(String|Buffer)?} passphrase - * @param {Function} callback + * @returns {Promise} */ -HTTPClient.prototype.retoken = function retoken(id, passphrase, callback) { - var options; - - if (typeof passphrase === 'function') { - callback = passphrase; - passphrase = null; - } - - options = { passphrase: passphrase }; - - this._post('/wallet/' + id + '/retoken', options, function(err, body) { - if (err) - return callback(err); - - return callback(null, body.token); - }); -}; +HTTPClient.prototype.retoken = co(function* retoken(id, passphrase) { + var options = { passphrase: passphrase }; + var body = yield this._post('/wallet/' + id + '/retoken', options); + return body.token; +}); /** * Change or set master key's passphrase. * @param {(String|Buffer)?} old * @param {String|Buffer} new_ - * @param {Function} callback + * @returns {Promise} */ -HTTPClient.prototype.setPassphrase = function setPassphrase(id, old, new_, callback) { +HTTPClient.prototype.setPassphrase = function setPassphrase(id, old, new_) { var options = { old: old, passphrase: new_ }; - - this._post('/wallet/' + id + '/passphrase', options, callback); + return this._post('/wallet/' + id + '/passphrase', options); }; /** * Create a transaction, fill. * @param {WalletID} id * @param {Object} options - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -HTTPClient.prototype.createTX = function createTX(id, options, callback) { +HTTPClient.prototype.createTX = function createTX(id, options) { options = utils.merge({}, options); if (options.rate) @@ -668,7 +604,7 @@ HTTPClient.prototype.createTX = function createTX(id, options, callback) { }; }); - this._post('/wallet/' + id + '/create', options, callback); + return this._post('/wallet/' + id + '/create', options); }; /** @@ -676,61 +612,46 @@ HTTPClient.prototype.createTX = function createTX(id, options, callback) { * @param {WalletID} id * @param {TX} tx * @param {Object} options - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -HTTPClient.prototype.sign = function sign(id, tx, options, callback) { +HTTPClient.prototype.sign = function sign(id, tx, options) { var body; - if (typeof options === 'function') { - callback = options; - options = null; - } - if (!options) options = {}; body = utils.merge({}, options); body.tx = toHex(tx); - this._post('/wallet/' + id + '/sign', body, callback); + return this._post('/wallet/' + id + '/sign', body); }; /** * Fill a transaction with coins. * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -HTTPClient.prototype.fillCoins = function fillCoins(id, tx, callback) { +HTTPClient.prototype.fillCoins = function fillCoins(id, tx) { var body = { tx: toHex(tx) }; - this._post('/wallet/' + id + '/fill', body, callback); + return this._post('/wallet/' + id + '/fill', body); }; /** * @param {WalletID} id * @param {Number} now - Current time. * @param {Number} age - Age delta (delete transactions older than `now - age`). - * @param {Function} callback + * @returns {Promise} */ -HTTPClient.prototype.zap = function zap(id, account, age, callback) { - var body; - - if (typeof age === 'function') { - callback = age; - age = account; - account = null; - } - - body = { +HTTPClient.prototype.zap = function zap(id, account, age) { + var body = { account: account, age: age }; - assert(utils.isNumber(age)); - - this._post('/wallet/' + id + '/zap', body, callback); + return this._post('/wallet/' + id + '/zap', body); }; /** @@ -739,22 +660,16 @@ HTTPClient.prototype.zap = function zap(id, account, age, callback) { * @param {(String|Number)?} account * @param {HDPublicKey|Base58String} key - Account (bip44) or * Purpose (bip45) key (can be in base58 form). - * @param {Function} callback + * @returns {Promise} */ -HTTPClient.prototype.addKey = function addKey(id, account, key, callback) { +HTTPClient.prototype.addKey = function addKey(id, account, key) { var options; - if (typeof key === 'function') { - callback = key; - key = account; - account = null; - } - key = key.xpubkey || key; options = { account: account, key: key }; - this._put('/wallet/' + id + '/key', options, callback); + return this._put('/wallet/' + id + '/key', options); }; /** @@ -763,62 +678,51 @@ HTTPClient.prototype.addKey = function addKey(id, account, key, callback) { * @param {(String|Number)?} account * @param {HDPublicKey|Base58String} key - Account (bip44) or Purpose * (bip45) key (can be in base58 form). - * @param {Function} callback + * @returns {Promise} */ -HTTPClient.prototype.removeKey = function removeKey(id, account, key, callback) { +HTTPClient.prototype.removeKey = function removeKey(id, account, key) { var options; - if (typeof key === 'function') { - callback = key; - key = account; - account = null; - } - key = key.xpubkey || key; options = { account: account, key: key }; - this._del('/wallet/' + id + '/key', options, callback); + return this._del('/wallet/' + id + '/key', options); }; /** * Get wallet accounts. * @param {WalletID} id - * @param {Function} callback - Returns [Error, Array]. + * @returns {Promise} - Returns Array. */ -HTTPClient.prototype.getAccounts = function getAccounts(id, callback) { +HTTPClient.prototype.getAccounts = function getAccounts(id) { var path = '/wallet/' + id + '/account'; - this._get(path, callback); + return this._get(path); }; /** * Get wallet account. * @param {WalletID} id * @param {String} account - * @param {Function} callback - Returns [Error, Array]. + * @returns {Promise} - Returns Array. */ -HTTPClient.prototype.getAccount = function getAccount(id, account, callback) { +HTTPClient.prototype.getAccount = function getAccount(id, account) { var path = '/wallet/' + id + '/account/' + account; - this._get(path, callback); + return this._get(path); }; /** * Create account. * @param {WalletID} id * @param {Object} options - * @param {Function} callback - Returns [Error, Array]. + * @returns {Promise} - Returns Array. */ -HTTPClient.prototype.createAccount = function createAccount(id, options, callback) { +HTTPClient.prototype.createAccount = function createAccount(id, options) { var path; - if (typeof options === 'function') { - callback = options; - options = null; - } - if (!options) options = {}; @@ -827,24 +731,19 @@ HTTPClient.prototype.createAccount = function createAccount(id, options, callbac path = '/wallet/' + id + '/account'; - this._post(path, options, callback); + return this._post(path, options); }; /** * Create address. * @param {WalletID} id * @param {Object} options - * @param {Function} callback - Returns [Error, Array]. + * @returns {Promise} - Returns Array. */ -HTTPClient.prototype.createAddress = function createAddress(id, options, callback) { +HTTPClient.prototype.createAddress = function createAddress(id, options) { var path; - if (typeof options === 'function') { - callback = options; - options = null; - } - if (!options) options = {}; @@ -853,7 +752,28 @@ HTTPClient.prototype.createAddress = function createAddress(id, options, callbac path = '/wallet/' + id + '/address'; - this._post(path, options, callback); + return this._post(path, options); +}; + +/** + * Create address. + * @param {WalletID} id + * @param {Object} options + * @returns {Promise} - Returns Array. + */ + +HTTPClient.prototype.createNested = function createNested(id, options) { + var path; + + if (!options) + options = {}; + + if (typeof options === 'string') + options = { account: options }; + + path = '/wallet/' + id + '/nested'; + + return this._post(path, options); }; /* diff --git a/lib/http/index.js b/lib/http/index.js index 124f1f60..42f54310 100644 --- a/lib/http/index.js +++ b/lib/http/index.js @@ -11,8 +11,12 @@ var utils = require('../utils/utils'); if (!utils.isBrowser) { exports.request = require('./request'); - exports.client = require('./client'); - exports.wallet = require('./wallet'); - exports.base = require('./base'); - exports.server = require('./server'); + exports.Client = require('./client'); + exports.RPCClient = require('./rpcclient'); + exports.Wallet = require('./wallet'); + exports.Base = require('./base'); + exports.RPC = require('./rpc'); + exports.Server = require('./server'); +} else { + exports.RPC = require('./rpc'); } diff --git a/lib/http/request.js b/lib/http/request.js index 950616b7..59fe4c36 100644 --- a/lib/http/request.js +++ b/lib/http/request.js @@ -14,7 +14,7 @@ */ var Stream = require('stream').Stream; -var assert = require('../utils/utils').assert; +var assert = require('assert'); // Spoof by default var USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1)' @@ -301,6 +301,17 @@ request._buffer = function(options, callback) { return stream; }; +request.promise = function promise(options) { + return new Promise(function(resolve, reject) { + request(options, function(err, res, body) { + if (err) + return reject(err); + res.body = body; + resolve(res); + }); + }); +}; + /* * ReqStream */ diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 53375c77..16e327ad 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -6,12 +6,29 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); +var co = require('../utils/co'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var ec = require('../crypto/ec'); +var time = require('../net/timedata'); var NetworkAddress = require('../primitives/netaddress'); +var Script = require('../script/script'); +var Address = require('../primitives/address'); +var Block = require('../primitives/block'); +var Coin = require('../primitives/coin'); +var Headers = require('../primitives/headers'); +var Input = require('../primitives/input'); +var KeyRing = require('../primitives/keyring'); +var Locker = require('../utils/locker'); +var MerkleBlock = require('../primitives/merkleblock'); +var MTX = require('../primitives/mtx'); +var Network = require('../protocol/network'); +var Outpoint = require('../primitives/outpoint'); +var Output = require('../primitives/output'); +var BufferReader = require('../utils/reader'); +var TX = require('../primitives/tx'); var EventEmitter = require('events').EventEmitter; var fs; @@ -38,7 +55,7 @@ function RPC(node) { this.walletdb = node.walletdb; this.logger = node.logger; - this.locker = new bcoin.locker(this); + this.locker = new Locker(); this.feeRate = null; this.mining = false; @@ -52,239 +69,239 @@ function RPC(node) { utils.inherits(RPC, EventEmitter); -RPC.prototype.execute = function execute(json, callback) { +RPC.prototype.execute = function execute(json) { switch (json.method) { case 'stop': - return this.stop(json.params, callback); + return this.stop(json.params); case 'help': - return this.help(json.params, callback); + return this.help(json.params); case 'getblockchaininfo': - return this.getblockchaininfo(json.params, callback); + return this.getblockchaininfo(json.params); case 'getbestblockhash': - return this.getbestblockhash(json.params, callback); + return this.getbestblockhash(json.params); case 'getblockcount': - return this.getblockcount(json.params, callback); + return this.getblockcount(json.params); case 'getblock': - return this.getblock(json.params, callback); + return this.getblock(json.params); case 'getblockhash': - return this.getblockhash(json.params, callback); + return this.getblockhash(json.params); case 'getblockheader': - return this.getblockheader(json.params, callback); + return this.getblockheader(json.params); case 'getchaintips': - return this.getchaintips(json.params, callback); + return this.getchaintips(json.params); case 'getdifficulty': - return this.getdifficulty(json.params, callback); + return this.getdifficulty(json.params); case 'getmempoolancestors': - return this.getmempoolancestors(json.params, callback); + return this.getmempoolancestors(json.params); case 'getmempooldescendants': - return this.getmempooldescendants(json.params, callback); + return this.getmempooldescendants(json.params); case 'getmempoolentry': - return this.getmempoolentry(json.params, callback); + return this.getmempoolentry(json.params); case 'getmempoolinfo': - return this.getmempoolinfo(json.params, callback); + return this.getmempoolinfo(json.params); case 'getrawmempool': - return this.getrawmempool(json.params, callback); + return this.getrawmempool(json.params); case 'gettxout': - return this.gettxout(json.params, callback); + return this.gettxout(json.params); case 'gettxoutsetinfo': - return this.gettxoutsetinfo(json.params, callback); + return this.gettxoutsetinfo(json.params); case 'verifychain': - return this.verifychain(json.params, callback); + return this.verifychain(json.params); case 'invalidateblock': - return this.invalidateblock(json.params, callback); + return this.invalidateblock(json.params); case 'reconsiderblock': - return this.reconsiderblock(json.params, callback); + return this.reconsiderblock(json.params); case 'getnetworkhashps': - return this.getnetworkhashps(json.params, callback); + return this.getnetworkhashps(json.params); case 'getmininginfo': - return this.getmininginfo(json.params, callback); + return this.getmininginfo(json.params); case 'prioritisetransaction': - return this.prioritisetransaction(json.params, callback); + return this.prioritisetransaction(json.params); case 'getwork': - return this.getwork(json.params, callback); + return this.getwork(json.params); case 'getworklp': - return this.getworklp(json.params, callback); + return this.getworklp(json.params); case 'getblocktemplate': - return this.getblocktemplate(json.params, callback); + return this.getblocktemplate(json.params); case 'submitblock': - return this.submitblock(json.params, callback); + return this.submitblock(json.params); case 'setgenerate': - return this.setgenerate(json.params, callback); + return this.setgenerate(json.params); case 'getgenerate': - return this.getgenerate(json.params, callback); + return this.getgenerate(json.params); case 'generate': - return this.generate(json.params, callback); + return this.generate(json.params); case 'generatetoaddress': - return this.generatetoaddress(json.params, callback); + return this.generatetoaddress(json.params); case 'estimatefee': - return this.estimatefee(json.params, callback); + return this.estimatefee(json.params); case 'estimatepriority': - return this.estimatepriority(json.params, callback); + return this.estimatepriority(json.params); case 'estimatesmartfee': - return this.estimatesmartfee(json.params, callback); + return this.estimatesmartfee(json.params); case 'estimatesmartpriority': - return this.estimatesmartpriority(json.params, callback); + return this.estimatesmartpriority(json.params); case 'getinfo': - return this.getinfo(json.params, callback); + return this.getinfo(json.params); case 'validateaddress': - return this.validateaddress(json.params, callback); + return this.validateaddress(json.params); case 'createmultisig': - return this.createmultisig(json.params, callback); + return this.createmultisig(json.params); case 'createwitnessaddress': - return this.createwitnessaddress(json.params, callback); + return this.createwitnessaddress(json.params); case 'verifymessage': - return this.verifymessage(json.params, callback); + return this.verifymessage(json.params); case 'signmessagewithprivkey': - return this.signmessagewithprivkey(json.params, callback); + return this.signmessagewithprivkey(json.params); case 'setmocktime': - return this.setmocktime(json.params, callback); + return this.setmocktime(json.params); case 'getconnectioncount': - return this.getconnectioncount(json.params, callback); + return this.getconnectioncount(json.params); case 'ping': - return this.ping(json.params, callback); + return this.ping(json.params); case 'getpeerinfo': - return this.getpeerinfo(json.params, callback); + return this.getpeerinfo(json.params); case 'addnode': - return this.addnode(json.params, callback); + return this.addnode(json.params); case 'disconnectnode': - return this.disconnectnode(json.params, callback); + return this.disconnectnode(json.params); case 'getaddednodeinfo': - return this.getaddednodeinfo(json.params, callback); + return this.getaddednodeinfo(json.params); case 'getnettotals': - return this.getnettotals(json.params, callback); + return this.getnettotals(json.params); case 'getnetworkinfo': - return this.getnetworkinfo(json.params, callback); + return this.getnetworkinfo(json.params); case 'setban': - return this.setban(json.params, callback); + return this.setban(json.params); case 'listbanned': - return this.listbanned(json.params, callback); + return this.listbanned(json.params); case 'clearbanned': - return this.clearbanned(json.params, callback); + return this.clearbanned(json.params); case 'getrawtransaction': - return this.getrawtransaction(json.params, callback); + return this.getrawtransaction(json.params); case 'createrawtransaction': - return this.createrawtransaction(json.params, callback); + return this.createrawtransaction(json.params); case 'decoderawtransaction': - return this.decoderawtransaction(json.params, callback); + return this.decoderawtransaction(json.params); case 'decodescript': - return this.decodescript(json.params, callback); + return this.decodescript(json.params); case 'sendrawtransaction': - return this.sendrawtransaction(json.params, callback); + return this.sendrawtransaction(json.params); case 'signrawtransaction': - return this.signrawtransaction(json.params, callback); + return this.signrawtransaction(json.params); case 'gettxoutproof': - return this.gettxoutproof(json.params, callback); + return this.gettxoutproof(json.params); case 'verifytxoutproof': - return this.verifytxoutproof(json.params, callback); + return this.verifytxoutproof(json.params); case 'fundrawtransaction': - return this.fundrawtransaction(json.params, callback); + return this.fundrawtransaction(json.params); case 'resendwallettransactions': - return this.resendwallettransactions(json.params, callback); + return this.resendwallettransactions(json.params); case 'abandontransaction': - return this.abandontransaction(json.params, callback); + return this.abandontransaction(json.params); case 'addmultisigaddress': - return this.addmultisigaddress(json.params, callback); + return this.addmultisigaddress(json.params); case 'addwitnessaddress': - return this.addwitnessaddress(json.params, callback); + return this.addwitnessaddress(json.params); case 'backupwallet': - return this.backupwallet(json.params, callback); + return this.backupwallet(json.params); case 'dumpprivkey': - return this.dumpprivkey(json.params, callback); + return this.dumpprivkey(json.params); case 'dumpwallet': - return this.dumpwallet(json.params, callback); + return this.dumpwallet(json.params); case 'encryptwallet': - return this.encryptwallet(json.params, callback); + return this.encryptwallet(json.params); case 'getaccountaddress': - return this.getaccountaddress(json.params, callback); + return this.getaccountaddress(json.params); case 'getaccount': - return this.getaccount(json.params, callback); + return this.getaccount(json.params); case 'getaddressesbyaccount': - return this.getaddressesbyaccount(json.params, callback); + return this.getaddressesbyaccount(json.params); case 'getbalance': - return this.getbalance(json.params, callback); + return this.getbalance(json.params); case 'getnewaddress': - return this.getnewaddress(json.params, callback); + return this.getnewaddress(json.params); case 'getrawchangeaddress': - return this.getrawchangeaddress(json.params, callback); + return this.getrawchangeaddress(json.params); case 'getreceivedbyaccount': - return this.getreceivedbyaccount(json.params, callback); + return this.getreceivedbyaccount(json.params); case 'getreceivedbyaddress': - return this.getreceivedbyaddress(json.params, callback); + return this.getreceivedbyaddress(json.params); case 'gettransaction': - return this.gettransaction(json.params, callback); + return this.gettransaction(json.params); case 'getunconfirmedbalance': - return this.getunconfirmedbalance(json.params, callback); + return this.getunconfirmedbalance(json.params); case 'getwalletinfo': - return this.getwalletinfo(json.params, callback); + return this.getwalletinfo(json.params); case 'importprivkey': - return this.importprivkey(json.params, callback); + return this.importprivkey(json.params); case 'importwallet': - return this.importwallet(json.params, callback); + return this.importwallet(json.params); case 'importaddress': - return this.importaddress(json.params, callback); + return this.importaddress(json.params); case 'importprunedfunds': - return this.importprunedfunds(json.params, callback); + return this.importprunedfunds(json.params); case 'importpubkey': - return this.importpubkey(json.params, callback); + return this.importpubkey(json.params); case 'keypoolrefill': - return this.keypoolrefill(json.params, callback); + return this.keypoolrefill(json.params); case 'listaccounts': - return this.listaccounts(json.params, callback); + return this.listaccounts(json.params); case 'listaddressgroupings': - return this.listaddressgroupings(json.params, callback); + return this.listaddressgroupings(json.params); case 'listlockunspent': - return this.listlockunspent(json.params, callback); + return this.listlockunspent(json.params); case 'listreceivedbyaccount': - return this.listreceivedbyaccount(json.params, callback); + return this.listreceivedbyaccount(json.params); case 'listreceivedbyaddress': - return this.listreceivedbyaddress(json.params, callback); + return this.listreceivedbyaddress(json.params); case 'listsinceblock': - return this.listsinceblock(json.params, callback); + return this.listsinceblock(json.params); case 'listtransactions': - return this.listtransactions(json.params, callback); + return this.listtransactions(json.params); case 'listunspent': - return this.listunspent(json.params, callback); + return this.listunspent(json.params); case 'lockunspent': - return this.lockunspent(json.params, callback); + return this.lockunspent(json.params); case 'move': - return this.move(json.params, callback); + return this.move(json.params); case 'sendfrom': - return this.sendfrom(json.params, callback); + return this.sendfrom(json.params); case 'sendmany': - return this.sendmany(json.params, callback); + return this.sendmany(json.params); case 'sendtoaddress': - return this.sendtoaddress(json.params, callback); + return this.sendtoaddress(json.params); case 'setaccount': - return this.setaccount(json.params, callback); + return this.setaccount(json.params); case 'settxfee': - return this.settxfee(json.params, callback); + return this.settxfee(json.params); case 'signmessage': - return this.signmessage(json.params, callback); + return this.signmessage(json.params); case 'walletlock': - return this.walletlock(json.params, callback); + return this.walletlock(json.params); case 'walletpassphrasechange': - return this.walletpassphrasechange(json.params, callback); + return this.walletpassphrasechange(json.params); case 'walletpassphrase': - return this.walletpassphrase(json.params, callback); + return this.walletpassphrase(json.params); case 'removeprunedfunds': - return this.removeprunedfunds(json.params, callback); + return this.removeprunedfunds(json.params); case 'getmemory': - return this.getmemory(json.params, callback); + return this.getmemory(json.params); default: - return callback(new Error('Method not found: ' + json.method + '.')); + return Promise.reject(new Error('Method not found: ' + json.method + '.')); } }; @@ -292,42 +309,39 @@ RPC.prototype.execute = function execute(json, callback) { * Overall control/query calls */ -RPC.prototype.getinfo = function getinfo(args, callback) { - var self = this; +RPC.prototype.getinfo = co(function* getinfo(args) { + var balance; if (args.help || args.length !== 0) - return callback(new RPCError('getinfo')); + throw new RPCError('getinfo'); - this.wallet.getBalance(function(err, balance) { - if (err) - return callback(err); + balance = yield this.wallet.getBalance(); - callback(null, { - version: constants.USER_VERSION, - protocolversion: constants.VERSION, - walletversion: 0, - balance: +utils.btc(balance.total), - blocks: self.chain.height, - timeoffset: bcoin.time.offset, - connections: self.pool.peers.all.length, - proxy: '', - difficulty: self._getDifficulty(), - testnet: self.network.type !== bcoin.network.main, - keypoololdest: 0, - keypoolsize: 0, - unlocked_until: self.wallet.master.until, - paytxfee: +utils.btc(self.network.getRate()), - relayfee: +utils.btc(self.network.getMinRelay()), - errors: '' - }); - }); -}; + return { + version: constants.USER_VERSION, + protocolversion: constants.VERSION, + walletversion: 0, + balance: +utils.btc(balance.total), + blocks: this.chain.height, + timeoffset: time.offset, + connections: this.pool.peers.all.length, + proxy: '', + difficulty: this._getDifficulty(), + testnet: this.network.type !== Network.main, + keypoololdest: 0, + keypoolsize: 0, + unlocked_until: this.wallet.master.until, + paytxfee: +utils.btc(this.network.getRate()), + relayfee: +utils.btc(this.network.getMinRelay()), + errors: '' + }; +}); -RPC.prototype.help = function help(args, callback) { +RPC.prototype.help = function help(args) { var json; if (args.length === 0) - return callback(null, 'Select a command.'); + return Promise.resolve('Select a command.'); json = { method: args[0], @@ -336,31 +350,32 @@ RPC.prototype.help = function help(args, callback) { json.params.help = true; - this.execute(json, callback); + return this.execute(json); }; -RPC.prototype.stop = function stop(args, callback) { +RPC.prototype.stop = function stop(args) { if (args.help || args.length !== 0) - return callback(new RPCError('stop')); + return Promise.reject(new RPCError('stop')); - callback(null, 'Stopping.'); this.node.close(); + + return Promise.resolve('Stopping.'); }; /* * P2P networking */ -RPC.prototype.getnetworkinfo = function getnetworkinfo(args, callback) { +RPC.prototype.getnetworkinfo = function getnetworkinfo(args) { if (args.help || args.length !== 0) - return callback(new RPCError('getnetworkinfo')); + return Promise.reject(new RPCError('getnetworkinfo')); - callback(null, { + return Promise.resolve({ version: constants.USER_VERSION, subversion: constants.USER_AGENT, protocolversion: constants.VERSION, localservices: this.pool.services, - timeoffset: bcoin.time.offset, + timeoffset: time.offset, connections: this.pool.peers.all.length, networks: [], relayfee: +utils.btc(this.network.getMinRelay()), @@ -369,11 +384,11 @@ RPC.prototype.getnetworkinfo = function getnetworkinfo(args, callback) { }); }; -RPC.prototype.addnode = function addnode(args, callback) { +RPC.prototype.addnode = function addnode(args) { var i, node, cmd, seed, addr, peer; if (args.help || args.length !== 2) - return callback(new RPCError('addnode "node" "add|remove|onetry"')); + return Promise.reject(new RPCError('addnode "node" "add|remove|onetry"')); node = toString(args[0]); cmd = toString(args[1]); @@ -400,14 +415,14 @@ RPC.prototype.addnode = function addnode(args, callback) { break; } - callback(null, null); + return Promise.resolve(null); }; -RPC.prototype.disconnectnode = function disconnectnode(args, callback) { +RPC.prototype.disconnectnode = function disconnectnode(args) { var node, addr, peer; if (args.help || args.length !== 1) - return callback(new RPCError('disconnectnode "node"')); + return Promise.reject(new RPCError('disconnectnode "node"')); node = toString(args[0]); addr = NetworkAddress.fromHostname(node, this.network); @@ -416,22 +431,22 @@ RPC.prototype.disconnectnode = function disconnectnode(args, callback) { if (peer) peer.destroy(); - callback(null, null); + return Promise.resolve(null); }; -RPC.prototype.getaddednodeinfo = function getaddednodeinfo(args, callback) { +RPC.prototype.getaddednodeinfo = function getaddednodeinfo(args) { var out = []; var i, host, addr, peer, peers; if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('getaddednodeinfo dummy ( "node" )')); + return Promise.reject(new RPCError('getaddednodeinfo dummy ( "node" )')); if (args.length === 2) { host = toString(args[1]); addr = NetworkAddress.fromHostname(host, this.network); peer = this.pool.peers.get(addr); if (!peer) - return callback(new RPCError('Node has not been added.')); + return Promise.reject(new RPCError('Node has not been added.')); peers = [peer]; } else { peers = this.pool.peers.all; @@ -453,20 +468,21 @@ RPC.prototype.getaddednodeinfo = function getaddednodeinfo(args, callback) { }); } - callback(null, out); + return Promise.resolve(out); }; -RPC.prototype.getconnectioncount = function getconnectioncount(args, callback) { +RPC.prototype.getconnectioncount = function getconnectioncount(args) { if (args.help || args.length !== 0) - return callback(new RPCError('getconnectioncount')); - callback(null, this.pool.peers.all.length); + return Promise.reject(new RPCError('getconnectioncount')); + + return Promise.resolve(this.pool.peers.all.length); }; -RPC.prototype.getnettotals = function getnettotals(args, callback) { +RPC.prototype.getnettotals = function getnettotals(args) { var i, sent, recv, peer; if (args.help || args.length > 0) - return callback(new RPCError('getnettotals')); + return Promise.reject(new RPCError('getnettotals')); sent = 0; recv = 0; @@ -477,19 +493,19 @@ RPC.prototype.getnettotals = function getnettotals(args, callback) { recv += peer.socket.bytesRead; } - callback(null, { + return Promise.resolve({ totalbytesrecv: recv, totalbytessent: sent, timemillis: utils.ms() }); }; -RPC.prototype.getpeerinfo = function getpeerinfo(args, callback) { +RPC.prototype.getpeerinfo = function getpeerinfo(args) { var peers = []; var i, peer; if (args.help || args.length !== 0) - return callback(new RPCError('getpeerinfo')); + return Promise.reject(new RPCError('getpeerinfo')); for (i = 0; i < this.pool.peers.all.length; i++) { peer = this.pool.peers.all[i]; @@ -516,28 +532,28 @@ RPC.prototype.getpeerinfo = function getpeerinfo(args, callback) { }); } - callback(null, peers); + return Promise.resolve(peers); }; -RPC.prototype.ping = function ping(args, callback) { +RPC.prototype.ping = function ping(args) { var i; if (args.help || args.length !== 0) - return callback(new RPCError('ping')); + return Promise.reject(new RPCError('ping')); for (i = 0; i < this.pool.peers.all.length; i++) this.pool.peers.all[i].sendPing(); - callback(null, null); + return Promise.resolve(null); }; -RPC.prototype.setban = function setban(args, callback) { +RPC.prototype.setban = function setban(args) { var host, ip; if (args.help || args.length < 2 || (args[1] !== 'add' && args[1] !== 'remove')) { - return callback(new RPCError( + return Promise.reject(new RPCError( 'setban "ip(/netmask)" "add|remove" (bantime) (absolute)')); } @@ -553,14 +569,14 @@ RPC.prototype.setban = function setban(args, callback) { break; } - callback(null, null); + return Promise.resolve(null); }; -RPC.prototype.listbanned = function listbanned(args, callback) { +RPC.prototype.listbanned = function listbanned(args) { var i, banned, keys, host, time; if (args.help || args.length !== 0) - return callback(new RPCError('listbanned')); + return Promise.reject(new RPCError('listbanned')); banned = []; keys = Object.keys(this.pool.hosts.misbehaving); @@ -576,16 +592,16 @@ RPC.prototype.listbanned = function listbanned(args, callback) { }); } - callback(null, banned); + return Promise.resolve(banned); }; -RPC.prototype.clearbanned = function clearbanned(args, callback) { +RPC.prototype.clearbanned = function clearbanned(args) { if (args.help || args.length !== 0) - return callback(new RPCError('clearbanned')); + return Promise.reject(new RPCError('clearbanned')); this.pool.hosts.clear(); - callback(null, null); + return Promise.resolve(null); }; RPC.prototype._deployment = function _deployment(id, version, status) { @@ -615,85 +631,67 @@ RPC.prototype._getSoftforks = function _getSoftforks() { ]; }; -RPC.prototype._getBIP9Softforks = function _getBIP9Softforks(callback) { - var self = this; +RPC.prototype._getBIP9Softforks = co(function* _getBIP9Softforks() { var forks = {}; var keys = Object.keys(this.network.deployments); + var i, id, deployment, state; - utils.forEachSerial(keys, function(id, next) { - var deployment = self.network.deployments[id]; - self.chain.getState(self.chain.tip, id, function(err, state) { - if (err) - return next(err); + for (i = 0; i < keys.length; i++) { + id = keys[i]; + deployment = this.network.deployments[id]; + state = yield this.chain.getState(this.chain.tip, id); - switch (state) { - case constants.thresholdStates.DEFINED: - state = 'defined'; - break; - case constants.thresholdStates.STARTED: - state = 'started'; - break; - case constants.thresholdStates.LOCKED_IN: - state = 'locked_in'; - break; - case constants.thresholdStates.ACTIVE: - state = 'active'; - break; - case constants.thresholdStates.FAILED: - state = 'failed'; - break; - } + switch (state) { + case constants.thresholdStates.DEFINED: + state = 'defined'; + break; + case constants.thresholdStates.STARTED: + state = 'started'; + break; + case constants.thresholdStates.LOCKED_IN: + state = 'locked_in'; + break; + case constants.thresholdStates.ACTIVE: + state = 'active'; + break; + case constants.thresholdStates.FAILED: + state = 'failed'; + break; + } - forks[id] = { - status: state, - bit: deployment.bit, - startTime: deployment.startTime, - timeout: deployment.timeout - }; + forks[id] = { + status: state, + bit: deployment.bit, + startTime: deployment.startTime, + timeout: deployment.timeout + }; + } - next(); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, forks); - }); -}; + return forks; +}); /* Block chain and UTXO */ -RPC.prototype.getblockchaininfo = function getblockchaininfo(args, callback) { - var self = this; - +RPC.prototype.getblockchaininfo = co(function* getblockchaininfo(args) { if (args.help || args.length !== 0) - return callback(new RPCError('getblockchaininfo')); + throw new RPCError('getblockchaininfo'); - this.chain.tip.getMedianTimeAsync(function(err, medianTime) { - if (err) - return callback(err); - - self._getBIP9Softforks(function(err, forks) { - if (err) - return callback(err); - - callback(null, { - chain: self.network.type, - blocks: self.chain.height, - headers: self.chain.bestHeight, - bestblockhash: utils.revHex(self.chain.tip.hash), - difficulty: self._getDifficulty(), - mediantime: medianTime, - verificationprogress: self.chain.getProgress(), - chainwork: self.chain.tip.chainwork.toString('hex', 64), - pruned: self.chain.db.options.prune, - softforks: self._getSoftforks(), - bip9_softforks: forks, - pruneheight: self.chain.db.prune - ? Math.max(0, self.chain.height - self.chain.db.keepBlocks) - : null - }); - }); - }); -}; + return { + chain: 'main', + blocks: this.chain.height, + headers: this.chain.bestHeight, + bestblockhash: utils.revHex(this.chain.tip.hash), + difficulty: this._getDifficulty(), + mediantime: yield this.chain.tip.getMedianTimeAsync(), + verificationprogress: this.chain.getProgress(), + chainwork: this.chain.tip.chainwork.toString('hex', 64), + pruned: this.chain.db.options.prune, + softforks: this._getSoftforks(), + bip9_softforks: yield this._getBIP9Softforks(), + pruneheight: this.chain.db.prune + ? Math.max(0, this.chain.height - this.chain.db.keepBlocks) + : null + }; +}); RPC.prototype._getDifficulty = function getDifficulty(entry) { var shift, diff; @@ -720,65 +718,58 @@ RPC.prototype._getDifficulty = function getDifficulty(entry) { return diff; }; -RPC.prototype.getbestblockhash = function getbestblockhash(args, callback) { +RPC.prototype.getbestblockhash = function getbestblockhash(args) { if (args.help || args.length !== 0) - return callback(new RPCError('getbestblockhash')); + return Promise.reject(new RPCError('getbestblockhash')); - callback(null, this.chain.tip.rhash); + return Promise.resolve(this.chain.tip.rhash); }; -RPC.prototype.getblockcount = function getblockcount(args, callback) { +RPC.prototype.getblockcount = function getblockcount(args) { if (args.help || args.length !== 0) - return callback(new RPCError('getblockcount')); + return Promise.reject(new RPCError('getblockcount')); - callback(null, this.chain.tip.height); + return Promise.resolve(this.chain.tip.height); }; -RPC.prototype.getblock = function getblock(args, callback) { - var self = this; - var hash, verbose; +RPC.prototype.getblock = co(function* getblock(args) { + var hash, verbose, entry, block; if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('getblock "hash" ( verbose )')); + throw new RPCError('getblock "hash" ( verbose )'); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); verbose = true; if (args.length > 1) verbose = toBool(args[1]); - this.chain.db.get(hash, function(err, entry) { - if (err) - return callback(err); + entry = yield this.chain.db.get(hash); - if (!entry) - return callback(new RPCError('Block not found')); + if (!entry) + throw new RPCError('Block not found'); - self.chain.db.getBlock(entry.hash, function(err, block) { - if (err) - return callback(err); + block = yield this.chain.db.getBlock(entry.hash); - if (!block) { - if (self.chain.db.options.spv) - return callback(new RPCError('Block not available (spv mode)')); + if (!block) { + if (this.chain.db.options.spv) + throw new RPCError('Block not available (spv mode)'); - if (self.chain.db.prune) - return callback(new RPCError('Block not available (pruned data)')); + if (this.chain.db.prune) + throw new RPCError('Block not available (pruned data)'); - return callback(new RPCError('Can\'t read block from disk')); - } + throw new RPCError('Can\'t read block from disk'); + } - if (!verbose) - return callback(null, block.toRaw().toString('hex')); + if (!verbose) + return block.toRaw().toString('hex'); - self._blockToJSON(entry, block, false, callback); - }); - }); -}; + return yield this._blockToJSON(entry, block, false); +}); RPC.prototype._txToJSON = function _txToJSON(tx) { var self = this; @@ -833,7 +824,7 @@ RPC.prototype._scriptToJSON = function scriptToJSON(script, hex) { out.hex = script.toJSON(); type = script.getType(); - out.type = bcoin.script.typesByVal[type]; + out.type = Script.typesByVal[type]; out.reqSigs = script.isMultisig() ? script.getSmall(0) : 1; @@ -844,252 +835,196 @@ RPC.prototype._scriptToJSON = function scriptToJSON(script, hex) { return out; }; -RPC.prototype.getblockhash = function getblockhash(args, callback) { - var height; +RPC.prototype.getblockhash = co(function* getblockhash(args) { + var height, entry; if (args.help || args.length !== 1) - return callback(new RPCError('getblockhash index')); + throw new RPCError('getblockhash index'); height = toNumber(args[0]); if (height < 0 || height > this.chain.height) - return callback(new RPCError('Block height out of range.')); + throw new RPCError('Block height out of range.'); - this.chain.db.get(height, function(err, entry) { - if (err) - return callback(err); + entry = yield this.chain.db.get(height); - if (!entry) - return callback(new RPCError('Not found.')); + if (!entry) + throw new RPCError('Not found.'); - callback(null, entry.rhash); - }); -}; + return entry.rhash; +}); -RPC.prototype.getblockheader = function getblockheader(args, callback) { - var self = this; - var hash, verbose; +RPC.prototype.getblockheader = co(function* getblockheader(args) { + var hash, verbose, entry; if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('getblockheader "hash" ( verbose )')); + throw new RPCError('getblockheader "hash" ( verbose )'); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); verbose = true; if (args.length > 1) verbose = toBool(args[1], true); - this.chain.db.get(hash, function(err, entry) { - if (err) - return callback(err); + entry = yield this.chain.db.get(hash); - if (!entry) - return callback(new RPCError('Block not found')); + if (!entry) + throw new RPCError('Block not found'); - if (!verbose) - return callback(null, entry.toRaw().toString('hex', 0, 80)); + if (!verbose) + return entry.toRaw().toString('hex', 0, 80); - self._headerToJSON(entry, callback); - }); -}; + return yield this._headerToJSON(entry); +}); -RPC.prototype._headerToJSON = function _headerToJSON(entry, callback) { +RPC.prototype._headerToJSON = co(function* _headerToJSON(entry) { + var medianTime = yield entry.getMedianTimeAsync(); + var nextHash = yield this.chain.db.getNextHash(entry.hash); + + return { + hash: utils.revHex(entry.hash), + confirmations: this.chain.height - entry.height + 1, + height: entry.height, + version: entry.version, + merkleroot: utils.revHex(entry.merkleRoot), + time: entry.ts, + mediantime: medianTime, + bits: entry.bits, + difficulty: this._getDifficulty(entry), + chainwork: entry.chainwork.toString('hex', 64), + previousblockhash: entry.prevBlock !== constants.NULL_HASH + ? utils.revHex(entry.prevBlock) + : null, + nextblockhash: nextHash ? utils.revHex(nextHash) : null + }; +}); + +RPC.prototype._blockToJSON = co(function* _blockToJSON(entry, block, txDetails) { var self = this; - entry.getMedianTimeAsync(function(err, medianTime) { - if (err) - return callback(err); + var medianTime = yield entry.getMedianTimeAsync(); + var nextHash = yield this.chain.db.getNextHash(entry.hash); - self.chain.db.getNextHash(entry.hash, function(err, nextHash) { - if (err) - return callback(err); + return { + hash: utils.revHex(entry.hash), + confirmations: this.chain.height - entry.height + 1, + strippedsize: block.getBaseSize(), + size: block.getSize(), + weight: block.getWeight(), + height: entry.height, + version: entry.version, + merkleroot: utils.revHex(entry.merkleRoot), + tx: block.txs.map(function(tx) { + if (txDetails) + return self._txToJSON(tx); + return tx.rhash; + }), + time: entry.ts, + mediantime: medianTime, + bits: entry.bits, + difficulty: this._getDifficulty(entry), + chainwork: entry.chainwork.toString('hex', 64), + previousblockhash: entry.prevBlock !== constants.NULL_HASH + ? utils.revHex(entry.prevBlock) + : null, + nextblockhash: nextHash ? utils.revHex(nextHash) : null + }; +}); - callback(null, { - hash: utils.revHex(entry.hash), - confirmations: self.chain.height - entry.height + 1, - height: entry.height, - version: entry.version, - merkleroot: utils.revHex(entry.merkleRoot), - time: entry.ts, - mediantime: medianTime, - bits: entry.bits, - difficulty: self._getDifficulty(entry), - chainwork: entry.chainwork.toString('hex', 64), - previousblockhash: entry.prevBlock !== constants.NULL_HASH - ? utils.revHex(entry.prevBlock) - : null, - nextblockhash: nextHash ? utils.revHex(nextHash) : null - }); - }); - }); -}; - -RPC.prototype._blockToJSON = function _blockToJSON(entry, block, txDetails, callback) { - var self = this; - entry.getMedianTimeAsync(function(err, medianTime) { - if (err) - return callback(err); - - self.chain.db.getNextHash(entry.hash, function(err, nextHash) { - if (err) - return callback(err); - - callback(null, { - hash: utils.revHex(entry.hash), - confirmations: self.chain.height - entry.height + 1, - strippedsize: block.getBaseSize(), - size: block.getSize(), - weight: block.getWeight(), - height: entry.height, - version: entry.version, - merkleroot: utils.revHex(entry.merkleRoot), - tx: block.txs.map(function(tx) { - if (txDetails) - return self._txToJSON(tx); - return tx.rhash; - }), - time: entry.ts, - mediantime: medianTime, - bits: entry.bits, - difficulty: self._getDifficulty(entry), - chainwork: entry.chainwork.toString('hex', 64), - previousblockhash: entry.prevBlock !== constants.NULL_HASH - ? utils.revHex(entry.prevBlock) - : null, - nextblockhash: nextHash ? utils.revHex(nextHash) : null - }); - }); - }); -}; - -RPC.prototype.getchaintips = function getchaintips(args, callback) { - var self = this; - var i, tips, orphans, prevs, result, orphan; +RPC.prototype.getchaintips = co(function* getchaintips(args) { + var i, tips, orphans, prevs, result; + var orphan, entries, entry, main, fork; if (args.help || args.length !== 0) - return callback(new RPCError('getchaintips')); + throw new RPCError('getchaintips'); tips = []; orphans = []; prevs = {}; result = []; - this.chain.db.getEntries(function(err, entries) { - if (err) - return callback(err); + entries = yield this.chain.db.getEntries(); - utils.forEachSerial(entries, function(entry, next) { - entry.isMainChain(function(err, main) { - if (err) - return next(err); + for (i = 0; i < entries.length; i++) { + entry = entries[i]; + main = yield entry.isMainChain(); + if (!main) { + orphans.push(entry); + prevs[entry.prevBlock] = true; + } + } - if (!main) { - orphans.push(entry); - prevs[entry.prevBlock] = true; - } + for (i = 0; i < orphans.length; i++) { + orphan = orphans[i]; + if (!prevs[orphan.hash]) + tips.push(orphan); + } - next(); - }); - }, function(err) { - if (err) - return callback(err); + tips.push(this.chain.tip); - for (i = 0; i < orphans.length; i++) { - orphan = orphans[i]; - if (!prevs[orphan.hash]) - tips.push(orphan); - } - - tips.push(self.chain.tip); - - utils.forEachSerial(tips, function(entry, next) { - self._findFork(entry, function(err, fork) { - if (err) - return next(err); - - entry.isMainChain(function(err, main) { - if (err) - return next(err); - - result.push({ - height: entry.height, - hash: entry.rhash, - branchlen: entry.height - fork.height, - status: main ? 'active' : 'valid-headers' - }); - - next(); - }); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, result); - }); + for (i = 0; i < tips.length; i++) { + entry = tips[i]; + fork = yield this._findFork(entry); + main = yield entry.isMainChain(); + result.push({ + height: entry.height, + hash: entry.rhash, + branchlen: entry.height - fork.height, + status: main ? 'active' : 'valid-headers' }); - }); -}; + } -RPC.prototype._findFork = function _findFork(entry, callback) { - (function next(err, entry) { - if (err) - return callback(err); + return result; +}); - if (!entry) - return callback(new Error('Fork not found.')); +RPC.prototype._findFork = co(function* _findFork(entry) { + while (entry) { + if (yield entry.isMainChain()) + return entry; + entry = yield entry.getPrevious(); + } + throw new Error('Fork not found.'); +}); - entry.isMainChain(function(err, main) { - if (err) - return callback(err); - - if (main) - return callback(null, entry); - - entry.getPrevious(next); - }); - })(null, entry); -}; - -RPC.prototype.getdifficulty = function getdifficulty(args, callback) { +RPC.prototype.getdifficulty = function getdifficulty(args) { if (args.help || args.length !== 0) - return callback(new RPCError('getdifficulty')); + return Promise.reject(new RPCError('getdifficulty')); - callback(null, this._getDifficulty()); + return Promise.resolve(this._getDifficulty()); }; -RPC.prototype.getmempoolinfo = function getmempoolinfo(args, callback) { +RPC.prototype.getmempoolinfo = function getmempoolinfo(args) { if (args.help || args.length !== 0) - return callback(new RPCError('getmempoolinfo')); + return Promise.reject(new RPCError('getmempoolinfo')); if (!this.mempool) - return callback(new RPCError('No mempool available.')); + return Promise.reject(new RPCError('No mempool available.')); - callback(null, { + return Promise.resolve({ size: this.mempool.totalTX, bytes: this.mempool.getSize(), usage: this.mempool.getSize(), maxmempool: constants.mempool.MAX_MEMPOOL_SIZE, - mempoolminfee: +utils.btc(this.mempool.minFeeRate) + mempoolminfee: +utils.btc(this.mempool.minRelay) }); }; -RPC.prototype.getmempoolancestors = function getmempoolancestors(args, callback) { +RPC.prototype.getmempoolancestors = function getmempoolancestors(args) { var i, hash, verbose, entry, entries; if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('getmempoolancestors txid (verbose)')); + return Promise.reject(new RPCError('getmempoolancestors txid (verbose)')); if (!this.mempool) - return callback(new RPCError('No mempool available.')); + return Promise.reject(new RPCError('No mempool available.')); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Invalid parameter.')); + return Promise.reject(new RPCError('Invalid parameter.')); if (args.length > 1) verbose = toBool(args[1], false); @@ -1097,7 +1032,7 @@ RPC.prototype.getmempoolancestors = function getmempoolancestors(args, callback) entry = this.mempool.getEntry(hash); if (!entry) - return callback(new RPCError('Transaction not in mempool.')); + return Promise.reject(new RPCError('Transaction not in mempool.')); entries = this.mempool.getAncestors(entry.tx); @@ -1109,22 +1044,22 @@ RPC.prototype.getmempoolancestors = function getmempoolancestors(args, callback) entries[i] = entries[i].tx.rhash; } - callback(null, entries); + return Promise.resolve(entries); }; -RPC.prototype.getmempooldescendants = function getmempooldescendants(args, callback) { +RPC.prototype.getmempooldescendants = function getmempooldescendants(args) { var i, hash, verbose, entry, entries; if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('getmempooldescendants txid (verbose)')); + return Promise.reject(new RPCError('getmempooldescendants txid (verbose)')); if (!this.mempool) - return callback(new RPCError('No mempool available.')); + return Promise.reject(new RPCError('No mempool available.')); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Invalid parameter.')); + return Promise.reject(new RPCError('Invalid parameter.')); if (args.length > 1) verbose = toBool(args[1], false); @@ -1132,7 +1067,7 @@ RPC.prototype.getmempooldescendants = function getmempooldescendants(args, callb entry = this.mempool.getEntry(hash); if (!entry) - return callback(new RPCError('Transaction not in mempool.')); + return Promise.reject(new RPCError('Transaction not in mempool.')); entries = this.mempool.getDescendants(entry.tx); @@ -1144,46 +1079,46 @@ RPC.prototype.getmempooldescendants = function getmempooldescendants(args, callb entries[i] = entries[i].tx.rhash; } - callback(null, entries); + return Promise.resolve(entries); }; -RPC.prototype.getmempoolentry = function getmempoolentry(args, callback) { +RPC.prototype.getmempoolentry = function getmempoolentry(args) { var hash, entry; if (args.help || args.length !== 1) - return callback(new RPCError('getmempoolentry txid')); + return Promise.reject(new RPCError('getmempoolentry txid')); if (!this.mempool) - return callback(new RPCError('No mempool available.')); + return Promise.reject(new RPCError('No mempool available.')); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Invalid parameter.')); + return Promise.reject(new RPCError('Invalid parameter.')); entry = this.mempool.getEntry(hash); if (!entry) - return callback(new RPCError('Transaction not in mempool.')); + return Promise.reject(new RPCError('Transaction not in mempool.')); - callback(null, this._entryToJSON(entry)); + return Promise.resolve(this._entryToJSON(entry)); }; -RPC.prototype.getrawmempool = function getrawmempool(args, callback) { +RPC.prototype.getrawmempool = function getrawmempool(args) { var verbose; if (args.help || args.length > 1) - return callback(new RPCError('getrawmempool ( verbose )')); + return Promise.reject(new RPCError('getrawmempool ( verbose )')); verbose = false; if (args.length > 0) verbose = toBool(args[0], false); - this._mempoolToJSON(verbose, callback); + return this._mempoolToJSON(verbose); }; -RPC.prototype._mempoolToJSON = function _mempoolToJSON(verbose, callback) { +RPC.prototype._mempoolToJSON = function _mempoolToJSON(verbose) { var out = {}; var i, hashes, hash, entry; @@ -1200,12 +1135,12 @@ RPC.prototype._mempoolToJSON = function _mempoolToJSON(verbose, callback) { out[entry.tx.rhash] = this._entryToJSON(entry); } - return callback(null, out); + return out; } hashes = this.mempool.getSnapshot(); - callback(null, hashes.map(utils.revHex)); + return hashes.map(utils.revHex); }; RPC.prototype._entryToJSON = function _entryToJSON(entry) { @@ -1228,18 +1163,17 @@ RPC.prototype._entryToJSON = function _entryToJSON(entry) { }; }; -RPC.prototype.gettxout = function gettxout(args, callback) { - var self = this; - var hash, index, mempool; +RPC.prototype.gettxout = co(function* gettxout(args) { + var hash, index, mempool, coin; if (args.help || args.length < 2 || args.length > 3) - return callback(new RPCError('gettxout "txid" n ( includemempool )')); + throw new RPCError('gettxout "txid" n ( includemempool )'); if (this.chain.db.options.spv) - return callback(new RPCError('Cannot get coins in SPV mode.')); + throw new RPCError('Cannot get coins in SPV mode.'); if (this.chain.db.options.prune) - return callback(new RPCError('Cannot get coins when pruned.')); + throw new RPCError('Cannot get coins when pruned.'); hash = toHash(args[0]); index = toNumber(args[1]); @@ -1249,150 +1183,125 @@ RPC.prototype.gettxout = function gettxout(args, callback) { mempool = toBool(args[2], true); if (!hash || index < 0) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); - function getCoin(callback) { - if (mempool) - return self.node.getCoin(hash, index, callback); - self.chain.db.getCoin(hash, index, callback); - } + if (mempool) + coin = yield this.node.getCoin(hash, index); + else + coin = yield this.chain.db.getCoin(hash, index); - getCoin(function(err, coin) { - if (err) - return callback(err); + if (!coin) + return null; - if (!coin) - return callback(null, null); + return { + bestblock: utils.revHex(this.chain.tip.hash), + confirmations: coin.getConfirmations(this.chain.height), + value: +utils.btc(coin.value), + scriptPubKey: this._scriptToJSON(coin.script, true), + version: coin.version, + coinbase: coin.coinbase + }; +}); - callback(null, { - bestblock: utils.revHex(self.chain.tip.hash), - confirmations: coin.getConfirmations(self.chain.height), - value: +utils.btc(coin.value), - scriptPubKey: self._scriptToJSON(coin.script, true), - version: coin.version, - coinbase: coin.coinbase - }); - }); -}; - -RPC.prototype.gettxoutproof = function gettxoutproof(args, callback) { - var self = this; +RPC.prototype.gettxoutproof = co(function* gettxoutproof(args) { var uniq = {}; - var i, txids, block, hash, last; + var i, txids, block, hash, last, tx, coins; - if (args.help || (args.length !== 1 && args.length !== 2)) { - return callback(new RPCError('gettxoutproof' - + ' ["txid",...] ( blockhash )')); - } + if (args.help || (args.length !== 1 && args.length !== 2)) + throw new RPCError('gettxoutproof ["txid",...] ( blockhash )'); if (this.chain.db.options.spv) - return callback(new RPCError('Cannot get coins in SPV mode.')); + throw new RPCError('Cannot get coins in SPV mode.'); if (this.chain.db.options.prune) - return callback(new RPCError('Cannot get coins when pruned.')); + throw new RPCError('Cannot get coins when pruned.'); txids = toArray(args[0]); block = args[1]; if (!txids || txids.length === 0) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); if (block) { block = toHash(block); if (!block) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); } for (i = 0; i < txids.length; i++) { hash = toHash(txids[i]); + if (!hash) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); + if (uniq[hash]) - return callback(new RPCError('Duplicate txid.')); + throw new RPCError('Duplicate txid.'); + uniq[hash] = true; txids[i] = hash; last = hash; } - function getBlock(callback) { - if (hash) - return self.chain.db.getBlock(hash, callback); - - if (self.chain.options.indexTX) { - return self.chain.db.getTX(last, function(err, tx) { - if (err) - return callback(err); - if (!tx) - return callback(); - self.chain.db.getBlock(tx.block, callback); - }); - } - - self.chain.db.getCoins(last, function(err, coins) { - if (err) - return callback(err); - - if (!coins) - return callback(); - - self.chain.db.getBlock(coins.height, callback); - }); + if (hash) { + block = yield this.chain.db.getBlock(hash); + } else if (this.chain.options.indexTX) { + tx = yield this.chain.db.getTX(last); + if (!tx) + return; + block = yield this.chain.db.getBlock(tx.block); + } else { + coins = yield this.chain.db.getCoins(last); + if (!coins) + return; + block = yield this.chain.db.getBlock(coins.height); } - getBlock(function(err, block) { - if (err) - return callback(err); + if (!block) + throw new RPCError('Block not found.'); - if (!block) - return callback(new RPCError('Block not found.')); + for (i = 0; i < txids.length; i++) { + if (!block.hasTX(txids[i])) + throw new RPCError('Block does not contain all txids.'); + } - for (i = 0; i < txids.length; i++) { - if (!block.hasTX(txids[i])) - return callback(new RPCError('Block does not contain all txids.')); - } + block = MerkleBlock.fromHashes(block, txids); - block = bcoin.merkleblock.fromHashes(block, txids); + return block.toRaw().toString('hex'); +}); - callback(null, block.toRaw().toString('hex')); - }); -}; - -RPC.prototype.verifytxoutproof = function verifytxoutproof(args, callback) { +RPC.prototype.verifytxoutproof = co(function* verifytxoutproof(args) { var res = []; - var i, block, hash; + var i, block, hash, entry; if (args.help || args.length !== 1) - return callback(new RPCError('verifytxoutproof "proof"')); + throw new RPCError('verifytxoutproof "proof"'); - block = bcoin.merkleblock.fromRaw(toString(args[0]), 'hex'); + block = MerkleBlock.fromRaw(toString(args[0]), 'hex'); if (!block.verify()) - return callback(null, res); + return res; - this.chain.db.get(block.hash('hex'), function(err, entry) { - if (err) - return callback(err); + entry = yield this.chain.db.get(block.hash('hex')); - if (!entry) - return callback(new RPCError('Block not found in chain.')); + if (!entry) + throw new RPCError('Block not found in chain.'); - for (i = 0; i < block.matches.length; i++) { - hash = block.matches[i]; - res.push(utils.revHex(hash)); - } + for (i = 0; i < block.matches.length; i++) { + hash = block.matches[i]; + res.push(utils.revHex(hash)); + } - callback(null, res); - }); -}; + return res; +}); -RPC.prototype.gettxoutsetinfo = function gettxoutsetinfo(args, callback) { +RPC.prototype.gettxoutsetinfo = function gettxoutsetinfo(args) { if (args.help || args.length !== 0) - return callback(new RPCError('gettxoutsetinfo')); + return Promise.reject(new RPCError('gettxoutsetinfo')); if (this.chain.db.options.spv) - return callback(new RPCError('Chain state not available in SPV mode.')); + return Promise.reject(new RPCError('Chainstate not available (SPV mode).')); - callback(null, { + return Promise.resolve({ height: this.chain.height, bestblock: this.chain.tip.rhash, transactions: this.chain.db.state.tx, @@ -1403,52 +1312,52 @@ RPC.prototype.gettxoutsetinfo = function gettxoutsetinfo(args, callback) { }); }; -RPC.prototype.verifychain = function verifychain(args, callback) { +RPC.prototype.verifychain = function verifychain(args) { if (args.help || args.length > 2) - return callback(new RPCError('verifychain ( checklevel numblocks )')); + return Promise.reject(new RPCError('verifychain ( checklevel numblocks )')); if (this.chain.db.options.spv) - return callback(new RPCError('Cannot verify chain in SPV mode.')); + return Promise.reject(new RPCError('Cannot verify chain in SPV mode.')); if (this.chain.db.options.prune) - return callback(new RPCError('Cannot verify chain when pruned.')); + return Promise.reject(new RPCError('Cannot verify chain when pruned.')); - callback(); + return null; }; /* * Mining */ -RPC.prototype._submitwork = function _submitwork(data, callback) { +RPC.prototype._submitwork = co(function* _submitwork(data) { var attempt = this.attempt; var block, header, cb, cur; if (data.length !== 128) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); if (!attempt) - return callback(null, false); + return false; data = data.slice(0, 80); reverseEndian(data); - header = bcoin.headers.fromAbbr(data); + header = Headers.fromAbbr(data); block = attempt.block; if (header.prevBlock !== block.prevBlock || header.bits !== block.bits) { - return callback(null, false); + return false; } if (!header.verify()) - return callback(null, false); + return false; cb = this.coinbase[header.merkleRoot]; if (!cb) - return callback(null, false); + return false; cur = block.txs[0]; block.txs[0] = cb; @@ -1458,7 +1367,7 @@ RPC.prototype._submitwork = function _submitwork(data, callback) { block.txs[0] = cur; attempt.updateMerkle(); this.logger.warning('Bad calculated merkle root for submitted work.'); - return callback(null, false); + return false; } block.nonce = header.nonce; @@ -1466,106 +1375,112 @@ RPC.prototype._submitwork = function _submitwork(data, callback) { block.mutable = false; block.txs[0].mutable = false; - this.chain.add(block, function(err) { - if (err) { - if (err.type === 'VerifyError') - return callback(null, false); - return callback(err); - } - return callback(null, true); - }); -}; + try { + yield this.chain.add(block); + } catch (err) { + if (err.type === 'VerifyError') + return false; + throw err; + } -RPC.prototype._getwork = function _getwork(callback) { + return true; +}); + +RPC.prototype._getwork = co(function* _getwork() { + var attempt = yield this._getAttempt(true); var data, abbr; - this._getAttempt(true, function(err, attempt) { - if (err) - return callback(err); + data = new Buffer(128); + data.fill(0); - data = new Buffer(128); - data.fill(0); + abbr = attempt.block.abbr(); + abbr.copy(data, 0); - abbr = attempt.block.abbr(); - abbr.copy(data, 0); + data[80] = 0x80; + data.writeUInt32BE(80 * 8, data.length - 4, true); - data[80] = 0x80; - data.writeUInt32BE(80 * 8, data.length - 4, true); + reverseEndian(data); - reverseEndian(data); + return { + data: data.toString('hex'), + target: attempt.target.toString('hex'), + height: attempt.height + }; +}); - callback(null, { - data: data.toString('hex'), - target: attempt.target.toString('hex'), - height: attempt.height - }); - }); -}; +RPC.prototype.getworklp = co(function* getworklp(args) { + yield this._onBlock(); + return yield this._getwork(); +}); -RPC.prototype.getworklp = function getworklp(args, callback) { - var self = this; - this.once('clear block', function() { - self._getwork(callback); - }); -}; +RPC.prototype.getwork = co(function* getwork(args) { + var unlock = yield this.locker.lock(); + var data, result; -RPC.prototype.getwork = function getwork(args, callback) { - var data; - - callback = this.locker.lock(getwork, [args, callback]); - - if (!callback) - return; - - if (args.length > 1) - return callback(new RPCError('getwork ( "data" )')); + if (args.length > 1) { + unlock(); + throw new RPCError('getwork ( "data" )'); + } if (args.length === 1) { - if (!utils.isHex(args[0])) - return callback(new RPCError('Invalid parameter.')); + if (!utils.isHex(args[0])) { + unlock(); + throw new RPCError('Invalid parameter.'); + } data = new Buffer(args[0], 'hex'); - return this._submitwork(data, callback); + try { + result = yield this._submitwork(data); + } catch (e) { + unlock(); + throw e; + } + + return result; } - return this._getwork(callback); -}; + try { + result = yield this._getwork(); + } catch (e) { + unlock(); + throw e; + } -RPC.prototype.submitblock = function submitblock(args, callback) { + unlock(); + return result; +}); + +RPC.prototype.submitblock = co(function* submitblock(args) { + var unlock = yield this.locker.lock(); var block; - callback = this.locker.lock(submitblock, [args, callback]); - - if (!callback) - return; - if (args.help || args.length < 1 || args.length > 2) { - return callback(new RPCError('submitblock "hexdata"' - + ' ( "jsonparametersobject" )')); + unlock(); + throw new RPCError('submitblock "hexdata" ( "jsonparametersobject" )'); } - block = bcoin.block.fromRaw(toString(args[0]), 'hex'); + block = Block.fromRaw(toString(args[0]), 'hex'); - this._submitblock(block, callback); -}; + return yield this._submitblock(block); +}); -RPC.prototype._submitblock = function submitblock(block, callback) { +RPC.prototype._submitblock = co(function* submitblock(block) { if (block.prevBlock !== this.chain.tip.hash) - return callback(null, 'rejected: inconclusive-not-best-prevblk'); + return 'rejected: inconclusive-not-best-prevblk'; - this.chain.add(block, function(err, total) { - if (err) { - if (err.type === 'VerifyError') - return callback(null, 'rejected: ' + err.reason); - return callback(err); - } - callback(null, null); - }); -}; + try { + yield this.chain.add(block); + } catch (err) { + if (err.type === 'VerifyError') + return 'rejected: ' + err.reason; + throw err; + } -RPC.prototype.getblocktemplate = function getblocktemplate(args, callback) { - var self = this; + return null; +}); + +RPC.prototype.getblocktemplate = co(function* getblocktemplate(args) { var mode = 'template'; var version = -1; var coinbase = true; @@ -1573,7 +1488,7 @@ RPC.prototype.getblocktemplate = function getblocktemplate(args, callback) { var coinbasevalue, coinbasetxn; if (args.help || args.length > 1) - return callback(new RPCError('getblocktemplate ( "jsonrequestobject" )')); + throw new RPCError('getblocktemplate ( "jsonrequestobject" )'); if (args.length === 1) { opt = args[0] || {}; @@ -1581,18 +1496,18 @@ RPC.prototype.getblocktemplate = function getblocktemplate(args, callback) { if (opt.mode != null) { mode = opt.mode; if (mode !== 'template' && mode !== 'proposal') - return callback(new RPCError('Invalid mode.')); + throw new RPCError('Invalid mode.'); } lpid = opt.longpollid; if (mode === 'proposal') { if (!utils.isHex(opt.data)) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); - block = bcoin.block.fromRaw(opt.data, 'hex'); + block = Block.fromRaw(opt.data, 'hex'); - return this._submitblock(block, callback); + return yield this._submitblock(block); } if (Array.isArray(opt.rules)) { @@ -1623,194 +1538,193 @@ RPC.prototype.getblocktemplate = function getblocktemplate(args, callback) { if (!this.network.selfConnect) { if (this.pool.peers.all.length === 0) - return callback(new RPCError('Bitcoin is not connected!')); + throw new RPCError('Bitcoin is not connected!'); if (!this.chain.isFull()) - return callback(new RPCError('Bitcoin is downloading blocks...')); + throw new RPCError('Bitcoin is downloading blocks...'); } - this._poll(lpid, function(err) { - if (err) - return callback(err); - self._tmpl(version, coinbase, rules, callback); - }); -}; + yield this._poll(lpid); -RPC.prototype._tmpl = function _tmpl(version, coinbase, rules, callback) { - var self = this; + return yield this._tmpl(version, coinbase, rules); +}); + +RPC.prototype._tmpl = co(function* _tmpl(version, coinbase, rules) { + var unlock = yield this.locker.lock(); var txs = []; var txIndex = {}; var i, j, tx, deps, input, dep, block, output, raw, rwhash; - var keys, vbavailable, vbrules, mutable, template; + var keys, vbavailable, vbrules, mutable, template, attempt; + var id, deployment, state; - callback = this.locker.lock(_tmpl, [version, coinbase, rules, callback]); + try { + attempt = yield this._getAttempt(false); + } catch (e) { + unlock(); + throw e; + } - if (!callback) - return; + block = attempt.block; - this._getAttempt(false, function(err, attempt) { - if (err) - return callback(err); + for (i = 1; i < block.txs.length; i++) { + tx = block.txs[i]; + txIndex[tx.hash('hex')] = i; + deps = []; - block = attempt.block; - - for (i = 1; i < block.txs.length; i++) { - tx = block.txs[i]; - txIndex[tx.hash('hex')] = i; - deps = []; - - for (j = 0; j < tx.inputs.length; j++) { - input = tx.inputs[j]; - dep = txIndex[input.prevout.hash]; - if (dep != null) - deps.push(dep); - } - - txs.push({ - data: tx.toRaw().toString('hex'), - txid: tx.rhash, - hash: tx.rwhash, - depends: deps, - fee: tx.getFee(), - sigops: tx.getSigops(), - weight: tx.getWeight() - }); + for (j = 0; j < tx.inputs.length; j++) { + input = tx.inputs[j]; + dep = txIndex[input.prevout.hash]; + if (dep != null) + deps.push(dep); } - keys = Object.keys(self.network.deployments); - vbavailable = {}; - vbrules = []; - mutable = ['time', 'transactions', 'prevblock']; - - if (version >= 2) - mutable.push('version/force'); - - utils.forEachSerial(keys, function(id, next) { - var deployment = self.network.deployments[id]; - self.chain.getState(self.chain.tip, id, function(err, state) { - if (err) - return next(err); - - switch (state) { - case constants.thresholdStates.DEFINED: - case constants.thresholdStates.FAILED: - break; - case constants.thresholdStates.LOCKED_IN: - block.version |= 1 << deployment.bit; - case constants.thresholdStates.STARTED: - vbavailable[id] = deployment.bit; - if (rules) { - if (rules.indexOf(id) === -1 && !deployment.force) - block.version &= ~(1 << deployment.bit); - } - break; - case constants.thresholdStates.ACTIVE: - vbrules.push(id); - if (rules) { - if (rules.indexOf(id) === -1 && !deployment.force) - return next(new RPCError('Client must support ' + id + '.')); - } - break; - } - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - block.version >>>= 0; - - template = { - capabilities: ['proposal'], - version: block.version, - rules: vbrules, - vbavailable: vbavailable, - vbrequired: 0, - previousblockhash: utils.revHex(block.prevBlock), - transactions: txs, - longpollid: self.chain.tip.rhash + utils.pad32(self._totalTX()), - target: utils.revHex(attempt.target.toString('hex')), - submitold: false, - mintime: block.ts, - maxtime: bcoin.now() + 2 * 60 * 60, - mutable: mutable, - noncerange: '00000000ffffffff', - sigoplimit: attempt.witness - ? constants.block.MAX_SIGOPS_WEIGHT - : constants.block.MAX_SIGOPS, - sizelimit: constants.block.MAX_SIZE, - weightlimit: constants.block.MAX_WEIGHT, - curtime: block.ts, - bits: utils.hex32(block.bits), - height: attempt.height - }; - - if (coinbase) { - tx = attempt.coinbase; - - // We don't include the commitment - // output (see bip145). - if (attempt.witness) { - output = tx.outputs.pop(); - assert(output.script.isCommitment()); - raw = tx.toRaw(); - rwhash = tx.rwhash; - tx.outputs.push(output); - } else { - raw = tx.toRaw(); - rwhash = tx.rwhash; - } - - template.coinbasetxn = { - data: raw.toString('hex'), - txid: tx.rhash, - hash: rwhash, - depends: [], - fee: 0, - sigops: tx.getSigops(), - weight: tx.getWeight() - }; - } else { - template.coinbaseaux = { - flags: attempt.coinbaseFlags.toString('hex') - }; - template.coinbasevalue = attempt.coinbase.getOutputValue(); - } - - if (attempt.witness) { - tx = attempt.coinbase; - output = tx.outputs[tx.outputs.length - 1]; - assert(output.script.isCommitment()); - template.default_witness_commitment = output.script.toJSON(); - } - - callback(null, template); + txs.push({ + data: tx.toRaw().toString('hex'), + txid: tx.rhash, + hash: tx.rwhash, + depends: deps, + fee: tx.getFee(), + sigops: tx.getSigops(), + weight: tx.getWeight() }); - }); -}; + } -RPC.prototype._poll = function _poll(lpid, callback) { + keys = Object.keys(this.network.deployments); + vbavailable = {}; + vbrules = []; + mutable = ['time', 'transactions', 'prevblock']; + + if (version >= 2) + mutable.push('version/force'); + + for (i = 0; i < keys.length; i++) { + id = keys[i]; + deployment = this.network.deployments[id]; + state = yield this.chain.getState(this.chain.tip, id); + + switch (state) { + case constants.thresholdStates.DEFINED: + case constants.thresholdStates.FAILED: + break; + case constants.thresholdStates.LOCKED_IN: + block.version |= 1 << deployment.bit; + case constants.thresholdStates.STARTED: + vbavailable[id] = deployment.bit; + if (rules) { + if (rules.indexOf(id) === -1 && !deployment.force) + block.version &= ~(1 << deployment.bit); + } + break; + case constants.thresholdStates.ACTIVE: + vbrules.push(id); + if (rules) { + if (rules.indexOf(id) === -1 && !deployment.force) { + unlock(); + throw new RPCError('Client must support ' + id + '.'); + } + } + break; + } + } + + block.version >>>= 0; + + template = { + capabilities: ['proposal'], + version: block.version, + rules: vbrules, + vbavailable: vbavailable, + vbrequired: 0, + previousblockhash: utils.revHex(block.prevBlock), + transactions: txs, + longpollid: this.chain.tip.rhash + utils.pad32(this._totalTX()), + target: utils.revHex(attempt.target.toString('hex')), + submitold: false, + mintime: block.ts, + maxtime: time.now() + 2 * 60 * 60, + mutable: mutable, + noncerange: '00000000ffffffff', + sigoplimit: attempt.witness + ? constants.block.MAX_SIGOPS_WEIGHT + : constants.block.MAX_SIGOPS, + sizelimit: constants.block.MAX_SIZE, + weightlimit: constants.block.MAX_WEIGHT, + curtime: block.ts, + bits: utils.hex32(block.bits), + height: attempt.height + }; + + if (coinbase) { + tx = attempt.coinbase; + + // We don't include the commitment + // output (see bip145). + if (attempt.witness) { + output = tx.outputs.pop(); + assert(output.script.isCommitment()); + raw = tx.toRaw(); + rwhash = tx.rwhash; + tx.outputs.push(output); + } else { + raw = tx.toRaw(); + rwhash = tx.rwhash; + } + + template.coinbasetxn = { + data: raw.toString('hex'), + txid: tx.rhash, + hash: rwhash, + depends: [], + fee: 0, + sigops: tx.getSigops(), + weight: tx.getWeight() + }; + } else { + template.coinbaseaux = { + flags: attempt.coinbaseFlags.toString('hex') + }; + template.coinbasevalue = attempt.coinbase.getOutputValue(); + } + + if (attempt.witness) { + tx = attempt.coinbase; + output = tx.outputs[tx.outputs.length - 1]; + assert(output.script.isCommitment()); + template.default_witness_commitment = output.script.toJSON(); + } + + unlock(); + return template; +}); + +RPC.prototype._poll = co(function* _poll(lpid) { var watched, lastTX; if (typeof lpid !== 'string') - return callback(); + return; if (lpid.length !== 74) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); watched = lpid.slice(0, 64); lastTX = +lpid.slice(64, 74); if (!utils.isHex(watched) || !utils.isNumber(lastTX)) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); watched = utils.revHex(watched); if (this.chain.tip.hash !== watched) - return callback(); + return; - this.once('clear block', callback); + yield this._onBlock(); +}); + +RPC.prototype._onBlock = function _onBlock() { + var self = this; + return new Promise(function(resolve, reject) { + self.once('clear block', resolve); + }); }; RPC.prototype._clearBlock = function _clearBlock() { @@ -1847,8 +1761,7 @@ RPC.prototype._bindChain = function _bindChain() { }); }; -RPC.prototype._getAttempt = function _getAttempt(update, callback) { - var self = this; +RPC.prototype._getAttempt = co(function* _getAttempt(update) { var attempt = this.attempt; this._bindChain(); @@ -1858,65 +1771,56 @@ RPC.prototype._getAttempt = function _getAttempt(update, callback) { attempt.updateNonce(); this.coinbase[attempt.block.merkleRoot] = attempt.coinbase.clone(); } - return callback(null, attempt); + return attempt; } - this.miner.createBlock(function(err, attempt) { - if (err) - return callback(err); + attempt = yield this.miner.createBlock(); - self.attempt = attempt; - self.start = utils.now(); - self.coinbase[attempt.block.merkleRoot] = attempt.coinbase.clone(); + this.attempt = attempt; + this.start = utils.now(); + this.coinbase[attempt.block.merkleRoot] = attempt.coinbase.clone(); - callback(null, attempt); - }); -}; + return attempt; +}); RPC.prototype._totalTX = function _totalTX() { return this.mempool ? this.mempool.totalTX : 0; }; -RPC.prototype.getmininginfo = function getmininginfo(args, callback) { - var self = this; +RPC.prototype.getmininginfo = co(function* getmininginfo(args) { + var block, hashps; if (args.help || args.length !== 0) - return callback(new RPCError('getmininginfo')); + throw new RPCError('getmininginfo'); - this.chain.db.getBlock(this.chain.tip.hash, function(err, block) { - if (err) - return callback(err); + block = yield this.chain.db.getBlock(this.chain.tip.hash); - if (!block) - return callback(new RPCError('Block not found.')); + if (!block) + throw new RPCError('Block not found.'); - self._hashps(120, -1, function(err, hashps) { - if (err) - return callback(err); + hashps = yield this._hashps(120, -1); - callback(null, { - blocks: self.chain.height, - currentblocksize: block.getSize(), - currentblocktx: block.txs.length, - difficulty: self._getDifficulty(), - errors: '', - genproclimit: self.proclimit, - networkhashps: hashps, - pooledtx: self._totalTX(), - testnet: self.network !== bcoin.network.main, - chain: 'main', - generate: self.mining - }); - }); - }); -}; + return { + blocks: this.chain.height, + currentblocksize: block.getSize(), + currentblocktx: block.txs.length, + difficulty: this._getDifficulty(), + errors: '', + genproclimit: this.proclimit, + networkhashps: hashps, + pooledtx: this._totalTX(), + testnet: this.network !== Network.main, + chain: 'main', + generate: this.mining + }; +}); -RPC.prototype.getnetworkhashps = function getnetworkhashps(args, callback) { +RPC.prototype.getnetworkhashps = function getnetworkhashps(args) { var lookup = 120; var height = -1; if (args.help || args.length > 2) - return callback(new RPCError('getnetworkhashps ( blocks height )')); + return Promise.reject(new RPCError('getnetworkhashps ( blocks height )')); if (args.length > 0) lookup = toNumber(args[0], 120); @@ -1924,34 +1828,34 @@ RPC.prototype.getnetworkhashps = function getnetworkhashps(args, callback) { if (args.length > 1) height = toNumber(args[1], -1); - this._hashps(lookup, height, callback); + return this._hashps(lookup, height); }; -RPC.prototype.prioritisetransaction = function prioritisetransaction(args, callback) { +RPC.prototype.prioritisetransaction = function prioritisetransaction(args) { var hash, pri, fee, entry; if (args.help || args.length !== 3) { - return callback(new RPCError('prioritisetransaction' + return Promise.reject(new RPCError('prioritisetransaction' + ' ')); } if (!this.mempool) - return callback(new RPCError('No mempool available.')); + return Promise.reject(new RPCError('No mempool available.')); hash = toHash(args[0]); pri = args[1]; fee = args[2]; if (!hash) - return callback(new RPCError('Invalid parameter')); + return Promise.reject(new RPCError('Invalid parameter')); if (!utils.isNumber(pri) || !utils.isNumber(fee)) - return callback(new RPCError('Invalid parameter')); + return Promise.reject(new RPCError('Invalid parameter')); entry = this.mempool.getEntry(hash); if (!entry) - return callback(new RPCError('Transaction not in mempool.')); + return Promise.reject(new RPCError('Transaction not in mempool.')); entry.priority += pri; entry.fees += fee; @@ -1962,80 +1866,62 @@ RPC.prototype.prioritisetransaction = function prioritisetransaction(args, callb if (entry.fees < 0) entry.fees = 0; - callback(null, true); + return Promise.resolve(true); }; -RPC.prototype._hashps = function _hashps(lookup, height, callback) { - var self = this; - var minTime, maxTime, pb0, time, workDiff, timeDiff, ps; +RPC.prototype._hashps = co(function* _hashps(lookup, height) { + var i, minTime, maxTime, workDiff, timeDiff, ps, tip, entry; - function getPB(callback) { - if (height >= 0 && height < self.chain.tip.height) - return self.chain.db.get(height, callback); - callback(null, self.chain.tip); + tip = this.chain.tip; + if (height >= 0 && height < this.chain.tip.height) + tip = yield this.chain.db.get(height); + + if (!tip) + return 0; + + if (lookup <= 0) + lookup = tip.height % this.network.pow.retargetInterval + 1; + + if (lookup > tip.height) + lookup = tip.height; + + minTime = tip.ts; + maxTime = minTime; + entry = tip; + + for (i = 0; i < lookup; i++) { + entry = yield entry.getPrevious(); + + if (!entry) + throw new RPCError('Not found.'); + + minTime = Math.min(entry.ts, minTime); + maxTime = Math.max(entry.ts, maxTime); } - getPB(function(err, pb) { - if (err) - return callback(err); + if (minTime === maxTime) + return 0; - if (!pb) - return callback(null, 0); + workDiff = tip.chainwork.sub(entry.chainwork); + timeDiff = maxTime - minTime; + ps = +workDiff.toString(10) / timeDiff; - if (lookup <= 0) - lookup = pb.height % self.network.pow.retargetInterval + 1; - - if (lookup > pb.height) - lookup = pb.height; - - minTime = pb.ts; - maxTime = minTime; - pb0 = pb; - - utils.forRangeSerial(0, lookup, function(i, next) { - pb0.getPrevious(function(err, entry) { - if (err) - return callback(err); - - if (!entry) - return callback(new RPCError('Not found.')); - - pb0 = entry; - time = pb0.ts; - minTime = Math.min(time, minTime); - maxTime = Math.max(time, maxTime); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - if (minTime === maxTime) - return callback(null, 0); - - workDiff = pb.chainwork.sub(pb0.chainwork); - timeDiff = maxTime - minTime; - ps = +workDiff.toString(10) / timeDiff; - - callback(null, ps); - }); - }); -}; + return ps; +}); /* * Coin generation */ -RPC.prototype.getgenerate = function getgenerate(args, callback) { +RPC.prototype.getgenerate = function getgenerate(args) { if (args.help || args.length !== 0) - return callback(new RPCError('getgenerate')); - callback(null, this.mining); + return Promise.reject(new RPCError('getgenerate')); + return Promise.resolve(this.mining); }; -RPC.prototype.setgenerate = function setgenerate(args, callback) { +RPC.prototype.setgenerate = function setgenerate(args) { if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('setgenerate mine ( proclimit )')); + return Promise.reject(new RPCError('setgenerate mine ( proclimit )')); this.mining = toBool(args[0]); this.proclimit = toNumber(args[1], 0); @@ -2045,83 +1931,69 @@ RPC.prototype.setgenerate = function setgenerate(args, callback) { else this.miner.stop(); - callback(null, this.mining); + return Promise.resolve(this.mining); }; -RPC.prototype.generate = function generate(args, callback) { +RPC.prototype.generate = co(function* generate(args) { + var unlock = yield this.locker.lock(); var numblocks; - callback = this.locker.lock(generate, [args, callback]); - - if (!callback) - return; - - if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('generate numblocks ( maxtries )')); + if (args.help || args.length < 1 || args.length > 2) { + unlock(); + throw new RPCError('generate numblocks ( maxtries )'); + } numblocks = toNumber(args[0], 1); - this._generate(numblocks, callback); -}; + return yield this._generate(numblocks); +}); -RPC.prototype._generate = function _generate(numblocks, callback, force) { - var self = this; +RPC.prototype._generate = co(function* _generate(numblocks) { var hashes = []; + var i, block; - utils.forRangeSerial(0, numblocks, function(i, next) { - self.miner.mineBlock(function(err, block) { - if (err) - return next(err); - hashes.push(block.rhash); - self.chain.add(block, next); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, hashes); - }); -}; + for (i = 0; i < numblocks; i++) { + block = yield this.miner.mineBlock(); + hashes.push(block.rhash); + yield this.chain.add(block); + } -RPC.prototype.generatetoaddress = function generatetoaddress(args, callback) { - var self = this; - var numblocks, address; + return hashes; +}); - callback = this.locker.lock(generatetoaddress, [args, callback]); - - if (!callback) - return; +RPC.prototype.generatetoaddress = co(function* generatetoaddress(args) { + var unlock = yield this.locker.lock(); + var numblocks, address, hashes; if (args.help || args.length < 2 || args.length > 3) { - return callback(new RPCError('generatetoaddress' - + ' numblocks address ( maxtries )')); + unlock(); + throw new RPCError('generatetoaddress numblocks address ( maxtries )'); } numblocks = toNumber(args[0], 1); address = this.miner.address; - this.miner.address = bcoin.address.fromBase58(toString(args[1])); + this.miner.address = Address.fromBase58(toString(args[1])); - this._generate(numblocks, function(err, hashes) { - if (err) - return callback(err); + hashes = yield this._generate(numblocks); - self.miner.address = address; + this.miner.address = address; - callback(null, hashes); - }); -}; + unlock(); + return hashes; +}); /* * Raw transactions */ -RPC.prototype.createrawtransaction = function createrawtransaction(args, callback) { +RPC.prototype.createrawtransaction = function createrawtransaction(args) { var inputs, sendTo, tx, locktime; var i, input, output, hash, index, sequence; var keys, addrs, key, value, address, b58; if (args.help || args.length < 2 || args.length > 3) { - return callback(new RPCError('createrawtransaction' + return Promise.reject(new RPCError('createrawtransaction' + ' [{"txid":"id","vout":n},...]' + ' {"address":amount,"data":"hex",...}' + ' ( locktime )')); @@ -2131,14 +2003,14 @@ RPC.prototype.createrawtransaction = function createrawtransaction(args, callbac sendTo = toObject(args[1]); if (!inputs || !sendTo) - return callback(new RPCError('Invalid parameter')); + return Promise.reject(new RPCError('Invalid parameter')); - tx = bcoin.tx(); + tx = new TX(); if (args.length > 2 && args[2] != null) { locktime = toNumber(args[2]); if (locktime < 0 || locktime > 0xffffffff) - return callback(new RPCError('Invalid parameter, locktime out of range')); + return Promise.reject(new RPCError('Locktime out of range')); tx.locktime = locktime; } @@ -2146,7 +2018,7 @@ RPC.prototype.createrawtransaction = function createrawtransaction(args, callbac input = inputs[i]; if (!input) - return callback(new RPCError('Invalid parameter')); + return Promise.reject(new RPCError('Invalid parameter')); hash = toHash(input.txid); index = input.vout; @@ -2158,16 +2030,16 @@ RPC.prototype.createrawtransaction = function createrawtransaction(args, callbac if (!hash || !utils.isNumber(index) || index < 0) { - return callback(new RPCError('Invalid parameter')); + return Promise.reject(new RPCError('Invalid parameter')); } if (utils.isNumber(input.sequence)) { sequence = toNumber(input.sequence); if (input.sequence < 0 || input.sequence > 0xffffffff) - return callback(new RPCError('Invalid parameter')); + return Promise.reject(new RPCError('Invalid parameter')); } - input = new bcoin.input({ + input = new Input({ prevout: { hash: utils.revHex(hash), index: index @@ -2187,23 +2059,23 @@ RPC.prototype.createrawtransaction = function createrawtransaction(args, callbac if (key === 'data') { value = new Buffer(value, 'hex'); - output = new bcoin.output({ + output = new Output({ value: 0, - script: bcoin.script.fromNulldata(value) + script: Script.fromNulldata(value) }); tx.outputs.push(output); continue; } - address = bcoin.address.fromBase58(key); + address = Address.fromBase58(key); b58 = address.toBase58(this.network); if (addrs[b58]) - return callback(new RPCError('Duplicate address')); + return Promise.reject(new RPCError('Duplicate address')); addrs[b58] = true; - output = new bcoin.output({ + output = new Output({ value: toSatoshi(value), address: address }); @@ -2211,142 +2083,126 @@ RPC.prototype.createrawtransaction = function createrawtransaction(args, callbac tx.outputs.push(output); } - callback(null, tx.toRaw().toString('hex')); + return Promise.resolve(tx.toRaw().toString('hex')); }; -RPC.prototype.decoderawtransaction = function decoderawtransaction(args, callback) { +RPC.prototype.decoderawtransaction = function decoderawtransaction(args) { var tx; if (args.help || args.length !== 1) - return callback(new RPCError('decoderawtransaction "hexstring"')); + return Promise.reject(new RPCError('decoderawtransaction "hexstring"')); - tx = bcoin.tx.fromRaw(toString(args[0]), 'hex'); + tx = TX.fromRaw(toString(args[0]), 'hex'); - callback(null, this._txToJSON(tx)); + return Promise.resolve(this._txToJSON(tx)); }; -RPC.prototype.decodescript = function decodescript(args, callback) { +RPC.prototype.decodescript = function decodescript(args) { var data, script, hash, address; if (args.help || args.length !== 1) - return callback(new RPCError('decodescript "hex"')); + return Promise.reject(new RPCError('decodescript "hex"')); data = toString(args[0]); - script = new bcoin.script(); + script = new Script(); if (data.length > 0) script.fromRaw(new Buffer(data, 'hex')); hash = crypto.hash160(script.toRaw()); - address = bcoin.address.fromHash(hash, bcoin.script.types.SCRIPTHASH); + address = Address.fromHash(hash, Script.types.SCRIPTHASH); script = this._scriptToJSON(script); script.p2sh = address.toBase58(this.network); - callback(null, script); + return Promise.resolve(script); }; -RPC.prototype.getrawtransaction = function getrawtransaction(args, callback) { - var self = this; - var hash, verbose, json; +RPC.prototype.getrawtransaction = co(function* getrawtransaction(args) { + var hash, verbose, json, tx; if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('getrawtransaction "txid" ( verbose )')); + throw new RPCError('getrawtransaction "txid" ( verbose )'); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); verbose = false; if (args.length > 1) verbose = Boolean(args[1]); - this.node.getTX(hash, function(err, tx) { - if (err) - return callback(err); + tx = yield this.node.getTX(hash); - if (!tx) - return callback(new RPCError('Transaction not found.')); + if (!tx) + throw new RPCError('Transaction not found.'); - if (!verbose) - return callback(null, tx.toRaw().toString('hex')); + if (!verbose) + throw tx.toRaw().toString('hex'); - json = self._txToJSON(tx); - json.hex = tx.toRaw().toString('hex'); + json = this._txToJSON(tx); + json.hex = tx.toRaw().toString('hex'); - callback(null, json); - }); -}; + return json; +}); -RPC.prototype.sendrawtransaction = function sendrawtransaction(args, callback) { +RPC.prototype.sendrawtransaction = function sendrawtransaction(args) { var tx; if (args.help || args.length < 1 || args.length > 2) { - return callback(new RPCError('sendrawtransaction' + return Promise.reject(new RPCError('sendrawtransaction' + ' "hexstring" ( allowhighfees )')); } if (!utils.isHex(args[0])) - return callback(new RPCError('Invalid parameter')); + return Promise.reject(new RPCError('Invalid parameter')); - tx = bcoin.tx.fromRaw(args[0], 'hex'); + tx = TX.fromRaw(args[0], 'hex'); this.node.sendTX(tx); - callback(null, tx.rhash); + return tx.rhash; }; -RPC.prototype.signrawtransaction = function signrawtransaction(args, callback) { - var self = this; +RPC.prototype.signrawtransaction = co(function* signrawtransaction(args) { var raw, p, txs, merged; if (args.help || args.length < 1 || args.length > 4) { - return callback(new RPCError('signrawtransaction' + throw new RPCError('signrawtransaction' + ' "hexstring" (' + ' [{"txid":"id","vout":n,"scriptPubKey":"hex",' + 'redeemScript":"hex"},...] ["privatekey1",...]' - + ' sighashtype )')); + + ' sighashtype )'); } if (!utils.isHex(args[0])) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); raw = new Buffer(args[0], 'hex'); - p = new bcoin.reader(raw); + p = new BufferReader(raw); txs = []; while (p.left()) - txs.push(bcoin.mtx.fromRaw(p)); + txs.push(MTX.fromRaw(p)); merged = txs[0]; - this._fillCoins(merged, function(err) { - if (err) - return callback(err); + yield this._fillCoins(merged); + yield this.wallet.fillCoins(merged); - self.wallet.fillCoins(merged, function(err) { - if (err) - return callback(err); + return yield this._signrawtransaction(merged, txs, args); +}); - try { - self._signrawtransaction(merged, txs, args, callback); - } catch (e) { - callback(e); - } - }); - }); -}; - -RPC.prototype._fillCoins = function _fillCoins(tx, callback) { +RPC.prototype._fillCoins = function _fillCoins(tx) { if (this.chain.db.options.spv) - return callback(); + return Promise.resolve(null); - this.node.fillCoins(tx, callback); + return this.node.fillCoins(tx); }; -RPC.prototype._signrawtransaction = function signrawtransaction(merged, txs, args, callback) { +RPC.prototype._signrawtransaction = co(function* signrawtransaction(merged, txs, args) { var keys = []; var keyMap = {}; var k, i, secret, key; @@ -2361,9 +2217,9 @@ RPC.prototype._signrawtransaction = function signrawtransaction(merged, txs, arg secret = k[i]; if (!utils.isBase58(secret)) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); - key = bcoin.keyring.fromSecret(secret); + key = KeyRing.fromSecret(secret); keyMap[key.getPublicKey('hex')] = key; keys.push(key); } @@ -2377,7 +2233,7 @@ RPC.prototype._signrawtransaction = function signrawtransaction(merged, txs, arg prev = prevout[i]; if (!prev) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); hash = toHash(prev.txid); index = prev.vout; @@ -2388,11 +2244,11 @@ RPC.prototype._signrawtransaction = function signrawtransaction(merged, txs, arg || !utils.isNumber(index) || index < 0 || !utils.isHex(script)) { - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); } - script = bcoin.script.fromRaw(script, 'hex'); - coins.push(new bcoin.coin({ + script = Script.fromRaw(script, 'hex'); + coins.push(new Coin({ hash: utils.revHex(hash), index: index, script: script, @@ -2405,7 +2261,7 @@ RPC.prototype._signrawtransaction = function signrawtransaction(merged, txs, arg continue; if (script.isScripthash() || script.isWitnessScripthash()) { - redeem = bcoin.script.fromRaw(prev.redeemScript, 'hex'); + redeem = Script.fromRaw(prev.redeemScript, 'hex'); for (j = 0; j < redeem.length; j++) { op = redeem.get(j); @@ -2430,12 +2286,12 @@ RPC.prototype._signrawtransaction = function signrawtransaction(merged, txs, arg parts = args[3].split('|'); type = constants.hashType[parts[0]]; if (type == null) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); if (parts.length > 2) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); if (parts.length === 2) { if (parts[1] !== 'ANYONECANPAY') - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); type |= constants.hashType.ANYONECANPAY; } } @@ -2445,38 +2301,36 @@ RPC.prototype._signrawtransaction = function signrawtransaction(merged, txs, arg merged.sign(key, type); } - this.wallet.sign(merged, { type: type }, function(err) { - if (err) - return callback(err); + yield this.wallet.sign(merged, { type: type }); - // TODO: Merge with other txs here. + // TODO: Merge with other txs here. - callback(null, { - hex: merged.toRaw().toString('hex'), - complete: merged.isSigned() - }); - }); -}; + return { + hex: merged.toRaw().toString('hex'), + complete: merged.isSigned() + }; +}); -RPC.prototype.fundrawtransaction = function fundrawtransaction(args, callback) { +RPC.prototype.fundrawtransaction = co(function* fundrawtransaction(args) { var tx, options, changeAddress, feeRate; - if (args.help || args.length < 1 || args.length > 2) { - return callback(new RPCError('fundrawtransaction' - + ' "hexstring" ( options )')); - } + if (args.help || args.length < 1 || args.length > 2) + throw new RPCError('fundrawtransaction "hexstring" ( options )'); - tx = bcoin.mtx.fromRaw(toString(args[0]), 'hex'); + tx = MTX.fromRaw(toString(args[0]), 'hex'); if (tx.outputs.length === 0) - return callback(new RPCError('TX must have at least one output.')); + throw new RPCError('TX must have at least one output.'); if (args.length === 2 && args[1]) { options = args[1]; changeAddress = toString(options.changeAddress); + if (changeAddress) - changeAddress = bcoin.address.fromBase58(changeAddress); + changeAddress = Address.fromBase58(changeAddress); + feeRate = options.feeRate; + if (feeRate != null) feeRate = toSatoshi(feeRate); } @@ -2486,175 +2340,158 @@ RPC.prototype.fundrawtransaction = function fundrawtransaction(args, callback) { changeAddress: changeAddress }; - this.wallet.fund(tx, options, function(err) { - if (err) - return callback(err); + yield this.wallet.fund(tx, options); - callback(null, { - hex: tx.toRaw().toString('hex'), - changepos: tx.changeIndex, - fee: +utils.btc(tx.getFee()) - }); - }); -}; + return { + hex: tx.toRaw().toString('hex'), + changepos: tx.changeIndex, + fee: +utils.btc(tx.getFee()) + }; +}); -RPC.prototype._createRedeem = function _createRedeem(args, callback) { - var self = this; - var m, n, keys, hash, script; +RPC.prototype._createRedeem = co(function* _createRedeem(args) { + var i, m, n, keys, hash, script, key, ring; if (!utils.isNumber(args[0]) || !Array.isArray(args[1]) || args[0] < 1 || args[1].length < args[0] || args[1].length > 16) { - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); } m = args[0]; n = args[1].length; keys = args[1]; - utils.forEachSerial(keys, function(key, next, i) { + for (i = 0; i < keys.length; i++) { + key = keys[i]; + if (!utils.isBase58(key)) { if (!utils.isHex(key)) - return next(new RPCError('Invalid key.')); + throw new RPCError('Invalid key.'); keys[i] = new Buffer(key, 'hex'); - return next(); + continue; } - hash = bcoin.address.getHash(key, 'hex'); + hash = Address.getHash(key, 'hex'); if (!hash) - return next(new RPCError('Invalid key.')); + throw new RPCError('Invalid key.'); - self.node.wallet.getKeyRing(hash, function(err, ring) { - if (err) - return next(err); + ring = yield this.node.wallet.getKey(hash); - if (!ring) - return next(new RPCError('Invalid key.')); + if (!ring) + throw new RPCError('Invalid key.'); - keys[i] = ring.publicKey; + keys[i] = ring.publicKey; + } - next(); - }); - }, function(err) { - if (err) - return callback(err); + try { + script = Script.fromMultisig(m, n, keys); + } catch (e) { + throw new RPCError('Invalid parameters.'); + } - try { - script = bcoin.script.fromMultisig(m, n, keys); - } catch (e) { - return callback(new RPCError('Invalid parameters.')); - } + if (script.toRaw().length > constants.script.MAX_PUSH) + throw new RPCError('Redeem script exceeds size limit.'); - if (script.toRaw().length > constants.script.MAX_PUSH) - return callback(new RPCError('Redeem script exceeds size limit.')); - - callback(null, script); - }); -}; + return script; +}); /* - * Utility functions + * Utility Functions */ -RPC.prototype.createmultisig = function createmultisig(args, callback) { - var self = this; +RPC.prototype.createmultisig = co(function* createmultisig(args) { + var script; if (args.help || args.length < 2 || args.length > 2) - return callback(new RPCError('createmultisig nrequired ["key",...]')); + throw new RPCError('createmultisig nrequired ["key",...]'); - this._createRedeem(args, function(err, script) { - if (err) - return callback(err); + script = yield this._createRedeem(args); - callback(null, { - address: script.getAddress().toBase58(self.network), - redeemScript: script.toJSON() - }); - }); -}; + return { + address: script.getAddress().toBase58(this.network), + redeemScript: script.toJSON() + }; +}); RPC.prototype._scriptForWitness = function scriptForWitness(script) { var hash; if (script.isPubkey()) { hash = crypto.hash160(script.get(0)); - return bcoin.script.fromProgram(0, hash); + return Script.fromProgram(0, hash); } if (script.isPubkeyhash()) { hash = script.get(2); - return bcoin.script.fromProgram(0, hash); + return Script.fromProgram(0, hash); } hash = script.sha256(); - return bcoin.script.fromProgram(0, hash); + return Script.fromProgram(0, hash); }; -RPC.prototype.createwitnessaddress = function createwitnessaddress(args, callback) { +RPC.prototype.createwitnessaddress = function createwitnessaddress(args) { var raw, script, program; if (args.help || args.length !== 1) - return callback(new RPCError('createwitnessaddress "script"')); + return Promise.reject(new RPCError('createwitnessaddress "script"')); raw = toString(args[1]); - script = bcoin.script.fromRaw(raw, 'hex'); + script = Script.fromRaw(raw, 'hex'); program = this._scriptForWitness(script); - callback(null, { + return Promise.resolve({ address: program.getAddress().toBase58(this.network), witnessScript: program.toJSON() }); }; -RPC.prototype.validateaddress = function validateaddress(args, callback) { - var self = this; - var b58, address, json; +RPC.prototype.validateaddress = co(function* validateaddress(args) { + var b58, address, json, path; if (args.help || args.length !== 1) - return callback(new RPCError('validateaddress "bitcoinaddress"')); + throw new RPCError('validateaddress "bitcoinaddress"'); b58 = toString(args[0]); try { - address = bcoin.address.fromBase58(b58); + address = Address.fromBase58(b58); } catch (e) { - return callback(null, { + return { isvalid: false - }); + }; } - this.wallet.getPath(address.getHash('hex'), function(err, path) { - if (err) - return callback(err); + path = yield this.wallet.getPath(address.getHash('hex')); - json = { - isvalid: true, - address: address.toBase58(self.network), - scriptPubKey: address.toScript().toJSON(), - ismine: path ? true : false, - iswatchonly: false - }; + json = { + isvalid: true, + address: address.toBase58(this.network), + scriptPubKey: address.toScript().toJSON(), + ismine: path ? true : false, + iswatchonly: false + }; - if (!path) - return callback(null, json); + if (!path) + return json; - json.account = path.name; - json.hdkeypath = path.toPath(); + json.account = path.name; + json.hdkeypath = path.toPath(); - callback(null, json); - }); -}; + return json; +}); RPC.magic = 'Bitcoin Signed Message:\n'; -RPC.prototype.verifymessage = function verifymessage(args, callback) { +RPC.prototype.verifymessage = function verifymessage(args) { var address, sig, msg, key; if (args.help || args.length !== 3) { - return callback(new RPCError('verifymessage' + return Promise.reject(new RPCError('verifymessage' + ' "bitcoinaddress" "signature" "message"')); } @@ -2662,51 +2499,53 @@ RPC.prototype.verifymessage = function verifymessage(args, callback) { sig = toString(args[1]); msg = toString(args[2]); - address = bcoin.address.getHash(address); + address = Address.getHash(address); if (!address) - return callback(new RPCError('Invalid address.')); + return Promise.reject(new RPCError('Invalid address.')); sig = new Buffer(sig, 'base64'); msg = new Buffer(RPC.magic + msg, 'utf8'); msg = crypto.hash256(msg); - key = bcoin.ec.recover(msg, sig, 0, true); + key = ec.recover(msg, sig, 0, true); if (!key) - return callback(null, false); + return Promise.resolve(false); key = crypto.hash160(key); - callback(null, crypto.ccmp(key, address)); + return Promise.resolve(crypto.ccmp(key, address)); }; -RPC.prototype.signmessagewithprivkey = function signmessagewithprivkey(args, callback) { +RPC.prototype.signmessagewithprivkey = function signmessagewithprivkey(args) { var key, msg, sig; - if (args.help || args.length !== 2) - return callback(new RPCError('signmessagewithprivkey "privkey" "message"')); + if (args.help || args.length !== 2) { + return Promise.reject(new RPCError( + 'signmessagewithprivkey "privkey" "message"')); + } key = toString(args[0]); msg = toString(args[1]); - key = bcoin.keyring.fromSecret(key); + key = KeyRing.fromSecret(key); msg = new Buffer(RPC.magic + msg, 'utf8'); msg = crypto.hash256(msg); sig = key.sign(msg); - callback(null, sig.toString('base64')); + return Promise.resolve(sig.toString('base64')); }; -RPC.prototype.estimatefee = function estimatefee(args, callback) { +RPC.prototype.estimatefee = function estimatefee(args) { var blocks, fee; if (args.help || args.length !== 1) - return callback(new RPCError('estimatefee nblocks')); + return Promise.reject(new RPCError('estimatefee nblocks')); if (!this.fees) - return callback(new RPCError('Fee estimation not available.')); + return Promise.reject(new RPCError('Fee estimation not available.')); blocks = toNumber(args[0], 1); @@ -2720,17 +2559,17 @@ RPC.prototype.estimatefee = function estimatefee(args, callback) { else fee = +utils.btc(fee); - callback(null, fee); + return Promise.resolve(fee); }; -RPC.prototype.estimatepriority = function estimatepriority(args, callback) { +RPC.prototype.estimatepriority = function estimatepriority(args) { var blocks, pri; if (args.help || args.length !== 1) - return callback(new RPCError('estimatepriority nblocks')); + return Promise.reject(new RPCError('estimatepriority nblocks')); if (!this.fees) - return callback(new RPCError('Priority estimation not available.')); + return Promise.reject(new RPCError('Priority estimation not available.')); blocks = toNumber(args[0], 1); @@ -2739,17 +2578,17 @@ RPC.prototype.estimatepriority = function estimatepriority(args, callback) { pri = this.fees.estimatePriority(blocks, false); - callback(null, pri); + return Promise.resolve(pri); }; -RPC.prototype.estimatesmartfee = function estimatesmartfee(args, callback) { +RPC.prototype.estimatesmartfee = function estimatesmartfee(args) { var blocks, fee; if (args.help || args.length !== 1) - return callback(new RPCError('estimatesmartfee nblocks')); + return Promise.reject(new RPCError('estimatesmartfee nblocks')); if (!this.fees) - return callback(new RPCError('Fee estimation not available.')); + return Promise.reject(new RPCError('Fee estimation not available.')); blocks = toNumber(args[0], 1); @@ -2763,20 +2602,20 @@ RPC.prototype.estimatesmartfee = function estimatesmartfee(args, callback) { else fee = +utils.btc(fee); - callback(null, { + return Promise.resolve({ fee: fee, blocks: blocks }); }; -RPC.prototype.estimatesmartpriority = function estimatesmartpriority(args, callback) { +RPC.prototype.estimatesmartpriority = function estimatesmartpriority(args) { var blocks, pri; if (args.help || args.length !== 1) - return callback(new RPCError('estimatesmartpriority nblocks')); + return Promise.reject(new RPCError('estimatesmartpriority nblocks')); if (!this.fees) - return callback(new RPCError('Priority estimation not available.')); + return Promise.reject(new RPCError('Priority estimation not available.')); blocks = toNumber(args[0], 1); @@ -2785,151 +2624,140 @@ RPC.prototype.estimatesmartpriority = function estimatesmartpriority(args, callb pri = this.fees.estimatePriority(blocks, true); - callback(null, { + return Promise.resolve({ priority: pri, blocks: blocks }); }; -RPC.prototype.invalidateblock = function invalidateblock(args, callback) { +RPC.prototype.invalidateblock = function invalidateblock(args) { var hash; if (args.help || args.length !== 1) - return callback(new RPCError('invalidateblock "hash"')); + return Promise.reject(new RPCError('invalidateblock "hash"')); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Block not found.')); + return Promise.reject(new RPCError('Block not found.')); this.chain.invalid[hash] = true; - callback(null, null); + return Promise.resolve(null); }; -RPC.prototype.reconsiderblock = function reconsiderblock(args, callback) { +RPC.prototype.reconsiderblock = function reconsiderblock(args) { var hash; if (args.help || args.length !== 1) - return callback(new RPCError('reconsiderblock "hash"')); + return Promise.reject(new RPCError('reconsiderblock "hash"')); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Block not found.')); + return Promise.reject(new RPCError('Block not found.')); delete this.chain.invalid[hash]; - callback(null, null); + return Promise.resolve(null); }; -RPC.prototype.setmocktime = function setmocktime(args, callback) { +RPC.prototype.setmocktime = function setmocktime(args) { var time, delta; if (args.help || args.length !== 1) - return callback(new RPCError('setmocktime timestamp')); + return Promise.reject(new RPCError('setmocktime timestamp')); time = toNumber(args[0]); if (time < 0) - return callback(new RPCError('Invalid parameter.')); + return Promise.reject(new RPCError('Invalid parameter.')); - delta = bcoin.now() - time; - bcoin.time.offset = -delta; + delta = time.now() - time; + time.offset = -delta; - callback(null, null); + return Promise.resolve(null); }; /* * Wallet */ -RPC.prototype.resendwallettransactions = function resendwallettransactions(args, callback) { +RPC.prototype.resendwallettransactions = co(function* resendwallettransactions(args) { var hashes = []; - var i, tx; + var i, tx, txs; if (args.help || args.length !== 0) - return callback(new RPCError('resendwallettransactions')); + throw new RPCError('resendwallettransactions'); - this.wallet.resend(function(err, txs) { - if (err) - return callback(err); + txs = yield this.wallet.resend(); - for (i = 0; i < txs.length; i++) { - tx = txs[i]; - hashes.push(tx.rhash); - } + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + hashes.push(tx.rhash); + } - callback(null, hashes); - }); -}; + return hashes; +}); -RPC.prototype.addmultisigaddress = function addmultisigaddress(args, callback) { +RPC.prototype.addmultisigaddress = function addmultisigaddress(args) { if (args.help || args.length < 2 || args.length > 3) { - return callback(new RPCError('addmultisigaddress' + return Promise.reject(new RPCError('addmultisigaddress' + ' nrequired ["key",...] ( "account" )')); } // Impossible to implement in bcoin (no address book). - callback(new Error('Not implemented.')); + Promise.reject(new Error('Not implemented.')); }; -RPC.prototype.addwitnessaddress = function addwitnessaddress(args, callback) { +RPC.prototype.addwitnessaddress = function addwitnessaddress(args) { if (args.help || args.length < 1 || args.length > 1) - return callback(new RPCError('addwitnessaddress "address"')); + return Promise.reject(new RPCError('addwitnessaddress "address"')); // Unlikely to be implemented. - callback(new Error('Not implemented.')); + Promise.reject(new Error('Not implemented.')); }; -RPC.prototype.backupwallet = function backupwallet(args, callback) { +RPC.prototype.backupwallet = co(function* backupwallet(args) { var dest; if (args.help || args.length !== 1) - return callback(new RPCError('backupwallet "destination"')); + throw new RPCError('backupwallet "destination"'); dest = toString(args[0]); - this.walletdb.backup(dest, function(err) { - if (err) - return callback(err); - callback(null, null); - }); -}; + yield this.walletdb.backup(dest); + return null; +}); -RPC.prototype.dumpprivkey = function dumpprivkey(args, callback) { - var self = this; - var hash; +RPC.prototype.dumpprivkey = co(function* dumpprivkey(args) { + var hash, ring; if (args.help || args.length !== 1) - return callback(new RPCError('dumpprivkey "bitcoinaddress"')); + throw new RPCError('dumpprivkey "bitcoinaddress"'); - hash = bcoin.address.getHash(toString(args[0]), 'hex'); + hash = Address.getHash(toString(args[0]), 'hex'); if (!hash) - return callback(new RPCError('Invalid address.')); + throw new RPCError('Invalid address.'); - this.wallet.getKeyRing(hash, function(err, ring) { - if (err) - return callback(err); + ring = yield this.wallet.getKey(hash); - if (!ring) - return callback(new RPCError('Key not found.')); + if (!ring) + throw new RPCError('Key not found.'); - if (!self.wallet.master.key) - return callback(new RPCError('Wallet is locked.')); + if (!this.wallet.master.key) + throw new RPCError('Wallet is locked.'); - callback(null, ring.toSecret()); - }); -}; + return ring.toSecret(); +}); -RPC.prototype.dumpwallet = function dumpwallet(args, callback) { - var self = this; - var file, time, address, fmt, str, out; +RPC.prototype.dumpwallet = co(function* dumpwallet(args) { + var i, file, time, address, fmt, str, out, hash, hashes, ring; if (args.help || args.length !== 1) - return callback(new RPCError('dumpwallet "filename"')); + throw new RPCError('dumpwallet "filename"'); if (!args[0] || typeof args[0] !== 'string') - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); file = toString(args[0]); time = utils.date(); @@ -2943,126 +2771,105 @@ RPC.prototype.dumpwallet = function dumpwallet(args, callback) { '' ]; - this.wallet.getAddressHashes(function(err, hashes) { - if (err) - return callback(err); + hashes = yield this.wallet.getAddressHashes(); - utils.forEachSerial(hashes, function(hash, next) { - self.wallet.getKeyRing(hash, function(err, ring) { - if (err) - return next(err); + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + ring = yield this.wallet.getKey(hash); - if (!ring) - return next(); + if (!ring) + continue; - if (!self.wallet.master.key) - return next(new RPCError('Wallet is locked.')); + if (!this.wallet.master.key) + throw new RPCError('Wallet is locked.'); - address = ring.getAddress('base58'); - fmt = '%s %s label= addr=%s'; + address = ring.getAddress('base58'); + fmt = '%s %s label= addr=%s'; - if (ring.change) - fmt = '%s %s change=1 addr=%s'; + if (ring.branch === 1) + fmt = '%s %s change=1 addr=%s'; - str = utils.fmt(fmt, ring.toSecret(), time, address); + str = utils.fmt(fmt, ring.toSecret(), time, address); - out.push(str); + out.push(str); + } - next(); - }); - }, function(err) { - if (err) - return callback(err); + out.push(''); + out.push('# End of dump'); + out.push(''); - out.push(''); - out.push('# End of dump'); - out.push(''); + out = out.join('\n'); - out = out.join('\n'); + if (!fs) + return out; - if (!fs) - return callback(null, out); + yield writeFile(file, out); - fs.writeFile(file, out, function(err) { - if (err) - return callback(err); - callback(null, out); - }); - }); - }); -}; + return out; +}); -RPC.prototype.encryptwallet = function encryptwallet(args, callback) { +RPC.prototype.encryptwallet = co(function* encryptwallet(args) { var passphrase; if (!this.wallet.master.encrypted && (args.help || args.help !== 1)) - return callback(new RPCError('encryptwallet "passphrase"')); + throw new RPCError('encryptwallet "passphrase"'); if (this.wallet.master.encrypted) - return callback(new RPCError('Already running with an encrypted wallet')); + throw new RPCError('Already running with an encrypted wallet'); passphrase = toString(args[0]); if (passphrase.length < 1) - return callback(new RPCError('encryptwallet "passphrase"')); + throw new RPCError('encryptwallet "passphrase"'); - this.wallet.setPassphrase(passphrase, function(err) { - if (err) - return callback(err); - callback(null, 'wallet encrypted; we do not need to stop!'); - }); -}; + yield this.wallet.setPassphrase(passphrase); -RPC.prototype.getaccountaddress = function getaccountaddress(args, callback) { + return 'wallet encrypted; we do not need to stop!'; +}); + +RPC.prototype.getaccountaddress = co(function* getaccountaddress(args) { var account; if (args.help || args.length !== 1) - return callback(new RPCError('getaccountaddress "account"')); + throw new RPCError('getaccountaddress "account"'); account = toString(args[0]); if (!account) account = 'default'; - this.wallet.getAccount(account, function(err, account) { - if (err) - return callback(err); + account = yield this.wallet.getAccount(account); - if (!account) - return callback(null, ''); + if (!account) + return ''; - callback(null, account.receiveAddress.getAddress('base58')); - }); -}; + return account.receive.getAddress('base58'); +}); -RPC.prototype.getaccount = function getaccount(args, callback) { - var hash; +RPC.prototype.getaccount = co(function* getaccount(args) { + var hash, path; if (args.help || args.length !== 1) - return callback(new RPCError('getaccount "bitcoinaddress"')); + throw new RPCError('getaccount "bitcoinaddress"'); - hash = bcoin.address.getHash(args[0], 'hex'); + hash = Address.getHash(args[0], 'hex'); if (!hash) - return callback(new RPCError('Invalid address.')); + throw new RPCError('Invalid address.'); - this.wallet.getPath(hash, function(err, path) { - if (err) - return callback(err); + path = yield this.wallet.getPath(hash); - if (!path) - return callback(null, ''); + if (!path) + return ''; - callback(null, path.name); - }); -}; + return path.name; +}); -RPC.prototype.getaddressesbyaccount = function getaddressesbyaccount(args, callback) { - var self = this; - var i, path, account, addrs; +RPC.prototype.getaddressesbyaccount = co(function* getaddressesbyaccount(args) { + var i, path, account, addrs, paths; if (args.help || args.length !== 1) - return callback(new RPCError('getaddressesbyaccount "account"')); + throw new RPCError('getaddressesbyaccount "account"'); account = toString(args[0]); @@ -3071,28 +2878,23 @@ RPC.prototype.getaddressesbyaccount = function getaddressesbyaccount(args, callb addrs = []; - this.wallet.getPaths(account, function(err, paths) { - if (err) - return callback(err); + paths = yield this.wallet.getPaths(account); - for (i = 0; i < paths.length; i++) { - path = paths[i]; - addrs.push(path.toAddress().toBase58(self.network)); - } - - callback(null, addrs); - }); -}; - -RPC.prototype.getbalance = function getbalance(args, callback) { - var minconf = 0; - var account, value; - - if (args.help || args.length > 3) { - return callback(new RPCError('getbalance' - + ' ( "account" minconf includeWatchonly )')); + for (i = 0; i < paths.length; i++) { + path = paths[i]; + addrs.push(path.toAddress().toBase58(this.network)); } + return addrs; +}); + +RPC.prototype.getbalance = co(function* getbalance(args) { + var minconf = 0; + var account, value, balance; + + if (args.help || args.length > 3) + throw new RPCError('getbalance ( "account" minconf includeWatchonly )'); + if (args.length >= 1) { account = toString(args[0]); if (!account) @@ -3104,24 +2906,21 @@ RPC.prototype.getbalance = function getbalance(args, callback) { if (args.length >= 2) minconf = toNumber(args[1], 0); - this.wallet.getBalance(account, function(err, balance) { - if (err) - return callback(err); + balance = yield this.wallet.getBalance(account); - if (minconf) - value = balance.confirmed; - else - value = balance.total; + if (minconf) + value = balance.confirmed; + else + value = balance.total; - callback(null, +utils.btc(value)); - }); -}; + return +utils.btc(value); +}); -RPC.prototype.getnewaddress = function getnewaddress(args, callback) { - var account; +RPC.prototype.getnewaddress = co(function* getnewaddress(args) { + var account, address; if (args.help || args.length > 1) - return callback(new RPCError('getnewaddress ( "account" )')); + throw new RPCError('getnewaddress ( "account" )'); if (args.length === 1) account = toString(args[0]); @@ -3129,34 +2928,31 @@ RPC.prototype.getnewaddress = function getnewaddress(args, callback) { if (!account) account = 'default'; - this.wallet.createReceive(account, function(err, address) { - if (err) - return callback(err); - callback(null, address.getAddress('base58')); - }); -}; + address = yield this.wallet.createReceive(account); + + return address.getAddress('base58'); +}); + +RPC.prototype.getrawchangeaddress = co(function* getrawchangeaddress(args) { + var address; -RPC.prototype.getrawchangeaddress = function getrawchangeaddress(args, callback) { if (args.help || args.length > 1) - return callback(new RPCError('getrawchangeaddress')); + throw new RPCError('getrawchangeaddress'); - this.wallet.createChange(function(err, address) { - if (err) - return callback(err); - callback(null, address.getAddress('base58')); - }); -}; + address = yield this.wallet.createChange(); -RPC.prototype.getreceivedbyaccount = function getreceivedbyaccount(args, callback) { - var self = this; + return address.getAddress('base58'); +}); + +RPC.prototype.getreceivedbyaccount = co(function* getreceivedbyaccount(args) { var minconf = 0; var total = 0; var filter = {}; var lastConf = -1; - var i, j, path, tx, output, conf, hash, account; + var i, j, path, tx, output, conf, hash, account, paths, txs; if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('getreceivedbyaccount "account" ( minconf )')); + throw new RPCError('getreceivedbyaccount "account" ( minconf )'); account = toString(args[0]); @@ -3166,265 +2962,233 @@ RPC.prototype.getreceivedbyaccount = function getreceivedbyaccount(args, callbac if (args.length === 2) minconf = toNumber(args[1], 0); - this.wallet.getPaths(account, function(err, paths) { - if (err) - return callback(err); + paths = yield this.wallet.getPaths(account); - for (i = 0; i < paths.length; i++) { - path = paths[i]; - filter[path.hash] = true; + for (i = 0; i < paths.length; i++) { + path = paths[i]; + filter[path.hash] = true; + } + + txs = yield this.wallet.getHistory(account); + + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + + if (minconf) { + if (tx.height === -1) + continue; + if (!(this.chain.height - tx.height + 1 >= minconf)) + continue; } - self.wallet.getHistory(account, function(err, txs) { - if (err) - return callback(err); + conf = tx.getConfirmations(this.chain.height); - for (i = 0; i < txs.length; i++) { - tx = txs[i]; + if (lastConf === -1 || conf < lastConf) + lastConf = conf; - if (minconf) { - if (tx.height === -1) - continue; - if (!(self.chain.height - tx.height + 1 >= minconf)) - continue; - } + for (j = 0; j < tx.outputs.length; j++) { + output = tx.outputs[j]; + hash = output.getHash('hex'); + if (filter[hash]) + total += output.value; + } + } - conf = tx.getConfirmations(self.chain.height); + return +utils.btc(total); +}); - if (lastConf === -1 || conf < lastConf) - lastConf = conf; - - for (j = 0; j < tx.outputs.length; j++) { - output = tx.outputs[j]; - hash = output.getHash('hex'); - if (filter[hash]) - total += output.value; - } - } - - callback(null, +utils.btc(total)); - }); - }); -}; - -RPC.prototype.getreceivedbyaddress = function getreceivedbyaddress(args, callback) { +RPC.prototype.getreceivedbyaddress = co(function* getreceivedbyaddress(args) { var self = this; var minconf = 0; var total = 0; - var i, j, hash, tx, output; + var i, j, hash, tx, output, txs; - if (args.help || args.length < 1 || args.length > 2) { - return callback(new RPCError('getreceivedbyaddress' - + ' "bitcoinaddress" ( minconf )')); - } + if (args.help || args.length < 1 || args.length > 2) + throw new RPCError('getreceivedbyaddress "bitcoinaddress" ( minconf )'); - hash = bcoin.address.getHash(toString(args[0]), 'hex'); + hash = Address.getHash(toString(args[0]), 'hex'); if (!hash) - return callback(new RPCError('Invalid address')); + throw new RPCError('Invalid address'); if (args.length === 2) minconf = toNumber(args[1], 0); - this.wallet.getHistory(function(err, txs) { - if (err) - return callback(err); - - for (i = 0; i < txs.length; i++) { - tx = txs[i]; - if (minconf) { - if (tx.height === -1) - continue; - if (!(self.chain.height - tx.height + 1 >= minconf)) - continue; - } - for (j = 0; j < tx.outputs.length; j++) { - output = tx.outputs[j]; - if (output.getHash('hex') === hash) - total += output.value; - } - } - - callback(null, +utils.btc(total)); - }); -}; - -RPC.prototype._toWalletTX = function _toWalletTX(tx, callback) { - var self = this; - var i, det, receive, member, sent, received, json; - - this.wallet.toDetails(tx, function(err, details) { - if (err) - return callback(err); - - if (!details) - return callback(new RPCError('TX not found.')); - - det = []; - sent = 0; - received = 0; - receive = true; - - for (i = 0; i < details.inputs.length; i++) { - member = details.inputs[i]; - if (member.path) { - receive = false; - break; - } - } - - for (i = 0; i < details.outputs.length; i++) { - member = details.outputs[i]; - - if (member.path) { - if (member.path.change === 1) - continue; - - det.push({ - account: member.path.name, - address: member.address.toBase58(self.network), - category: 'receive', - amount: +utils.btc(member.value), - label: member.path.name, - vout: i - }); - - received += member.value; + txs = yield this.wallet.getHistory(); + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + if (minconf) { + if (tx.height === -1) continue; - } + if (!(self.chain.height - tx.height + 1 >= minconf)) + continue; + } + for (j = 0; j < tx.outputs.length; j++) { + output = tx.outputs[j]; + if (output.getHash('hex') === hash) + total += output.value; + } + } - if (receive) + return +utils.btc(total); +}); + +RPC.prototype._toWalletTX = co(function* _toWalletTX(tx) { + var i, det, receive, member, sent, received, json, details; + + details = yield this.wallet.toDetails(tx); + + if (!details) + throw new RPCError('TX not found.'); + + det = []; + sent = 0; + received = 0; + receive = true; + + for (i = 0; i < details.inputs.length; i++) { + member = details.inputs[i]; + if (member.path) { + receive = false; + break; + } + } + + for (i = 0; i < details.outputs.length; i++) { + member = details.outputs[i]; + + if (member.path) { + if (member.path.branch === 1) continue; det.push({ - account: '', - address: member.address - ? member.address.toBase58(self.network) - : null, - category: 'send', - amount: -(+utils.btc(member.value)), - fee: -(+utils.btc(details.fee)), + account: member.path.name, + address: member.address.toBase58(this.network), + category: 'receive', + amount: +utils.btc(member.value), + label: member.path.name, vout: i }); - sent += member.value; + received += member.value; + + continue; } - json = { - amount: +utils.btc(receive ? received : -sent), - confirmations: details.confirmations, - blockhash: details.block ? utils.revHex(details.block) : null, - blockindex: details.index, - blocktime: details.ts, - txid: utils.revHex(details.hash), - walletconflicts: [], - time: details.ps, - timereceived: details.ps, - 'bip125-replaceable': 'no', - details: det, - hex: details.tx.toRaw().toString('hex') - }; + if (receive) + continue; - callback(null, json); - }); -}; + det.push({ + account: '', + address: member.address + ? member.address.toBase58(this.network) + : null, + category: 'send', + amount: -(+utils.btc(member.value)), + fee: -(+utils.btc(details.fee)), + vout: i + }); -RPC.prototype.gettransaction = function gettransaction(args, callback) { - var self = this; - var hash; + sent += member.value; + } + + json = { + amount: +utils.btc(receive ? received : -sent), + confirmations: details.confirmations, + blockhash: details.block ? utils.revHex(details.block) : null, + blockindex: details.index, + blocktime: details.ts, + txid: utils.revHex(details.hash), + walletconflicts: [], + time: details.ps, + timereceived: details.ps, + 'bip125-replaceable': 'no', + details: det, + hex: details.tx.toRaw().toString('hex') + }; + + return json; +}); + +RPC.prototype.gettransaction = co(function* gettransaction(args) { + var hash, tx; if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('gettransaction "txid" ( includeWatchonly )')); + throw new RPCError('gettransaction "txid" ( includeWatchonly )'); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); - this.wallet.getTX(hash, function(err, tx) { - if (err) - return callback(err); + tx = yield this.wallet.getTX(hash); - if (!tx) - return callback(new RPCError('TX not found.')); + if (!tx) + throw new RPCError('TX not found.'); - self._toWalletTX(tx, callback); - }); -}; + return yield this._toWalletTX(tx); +}); -RPC.prototype.abandontransaction = function abandontransaction(args, callback) { - var hash; +RPC.prototype.abandontransaction = co(function* abandontransaction(args) { + var hash, result; if (args.help || args.length !== 1) - return callback(new RPCError('abandontransaction "txid"')); + throw new RPCError('abandontransaction "txid"'); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); - this.wallet.abandon(hash, function(err, result) { - if (err) - return callback(err); + result = yield this.wallet.abandon(hash); - if (!result) - return callback(new RPCError('Transaction not in wallet.')); + if (!result) + throw new RPCError('Transaction not in wallet.'); - callback(null, null); - }); -}; + return null; +}); + +RPC.prototype.getunconfirmedbalance = co(function* getunconfirmedbalance(args) { + var balance; -RPC.prototype.getunconfirmedbalance = function getunconfirmedbalance(args, callback) { if (args.help || args.length > 0) - return callback(new RPCError('getunconfirmedbalance')); + throw new RPCError('getunconfirmedbalance'); - this.wallet.getBalance(function(err, balance) { - if (err) - return callback(err); + balance = yield this.wallet.getBalance(); - callback(null, +utils.btc(balance.unconfirmed)); - }); -}; + return +utils.btc(balance.unconfirmed); +}); -RPC.prototype.getwalletinfo = function getwalletinfo(args, callback) { - var self = this; +RPC.prototype.getwalletinfo = co(function* getwalletinfo(args) { + var balance, hashes; if (args.help || args.length !== 0) - return callback(new RPCError('getwalletinfo')); + throw new RPCError('getwalletinfo'); - this.wallet.getBalance(function(err, balance) { - if (err) - return callback(err); + balance = yield this.wallet.getBalance(); - self.wallet.tx.getHistoryHashes(self.wallet.id, function(err, hashes) { - if (err) - return callback(err); + hashes = yield this.wallet.tx.getHistoryHashes(this.wallet.id); - callback(null, { - walletversion: 0, - balance: +utils.btc(balance.total), - unconfirmed_balance: +utils.btc(balance.unconfirmed), - txcount: hashes.length, - keypoololdest: 0, - keypoolsize: 0, - unlocked_until: self.wallet.master.until, - paytxfee: self.feeRate != null - ? +utils.btc(self.feeRate) - : +utils.btc(0) - }); - }); - }); -}; + return { + walletversion: 0, + balance: +utils.btc(balance.total), + unconfirmed_balance: +utils.btc(balance.unconfirmed), + txcount: hashes.length, + keypoololdest: 0, + keypoolsize: 0, + unlocked_until: this.wallet.master.until, + paytxfee: this.feeRate != null + ? +utils.btc(this.feeRate) + : +utils.btc(0) + }; +}); -RPC.prototype.importprivkey = function importprivkey(args, callback) { - var self = this; +RPC.prototype.importprivkey = co(function* importprivkey(args) { var secret, label, rescan, key; - if (args.help || args.length < 1 || args.length > 3) { - return callback(new RPCError('importprivkey' - + ' "bitcoinprivkey" ( "label" rescan )')); - } + if (args.help || args.length < 1 || args.length > 3) + throw new RPCError('importprivkey "bitcoinprivkey" ( "label" rescan )'); secret = toString(args[0]); @@ -3435,107 +3199,90 @@ RPC.prototype.importprivkey = function importprivkey(args, callback) { rescan = toBool(args[2]); if (rescan && this.chain.db.options.prune) - return callback(new RPCError('Cannot rescan when pruned.')); + throw new RPCError('Cannot rescan when pruned.'); - key = bcoin.keyring.fromSecret(secret); + key = KeyRing.fromSecret(secret); - this.wallet.importKey(0, key, null, function(err) { - if (err) - return callback(err); + yield this.wallet.importKey(0, key, null); - if (!rescan) - return callback(null, null); + if (!rescan) + return null; - self.walletdb.rescan(self.chain.db, 0, function(err) { - if (err) - return callback(err); - callback(null, null); - }); - }); -}; + yield this.walletdb.rescan(this.chain.db, 0); -RPC.prototype.importwallet = function importwallet(args, callback) { - var self = this; + return null; +}); + +RPC.prototype.importwallet = co(function* importwallet(args) { var file, keys, lines, line, parts; var i, secret, time, label, addr; + var data, key; if (args.help || args.length !== 1) - return callback(new RPCError('importwallet "filename"')); + throw new RPCError('importwallet "filename"'); file = toString(args[0]); if (!fs) - return callback(new RPCError('FS not available.')); + throw new RPCError('FS not available.'); - fs.readFile(file, 'utf8', function(err, data) { - if (err) - return callback(err); + data = yield readFile(file, 'utf8'); - lines = data.split(/\n+/); - keys = []; + lines = data.split(/\n+/); + keys = []; - for (i = 0; i < lines.length; i++) { - line = lines[i].trim(); + for (i = 0; i < lines.length; i++) { + line = lines[i].trim(); - if (line.length === 0) - continue; + if (line.length === 0) + continue; - if (/^\s*#/.test(line)) - continue; + if (/^\s*#/.test(line)) + continue; - parts = line.split(/\s+/); + parts = line.split(/\s+/); - if (parts.length < 4) - return callback(new RPCError('Malformed wallet.')); + if (parts.length < 4) + throw new RPCError('Malformed wallet.'); - try { - secret = bcoin.keyring.fromSecret(parts[0]); - } catch (e) { - return callback(e); - } + secret = KeyRing.fromSecret(parts[0]); - time = +parts[1]; - label = parts[2]; - addr = parts[3]; + time = +parts[1]; + label = parts[2]; + addr = parts[3]; - keys.push(secret); - } + keys.push(secret); + } - utils.forEachSerial(keys, function(key, next) { - self.wallet.importKey(0, key, null, next); - }, function(err) { - if (err) - return callback(err); + for (i = 0; i < keys.length; i++) { + key = keys[i]; + yield this.wallet.importKey(0, key, null); + } - self.walletdb.rescan(self.chain.db, 0, function(err) { - if (err) - return callback(err); - callback(null, null); - }); - }); - }); -}; + yield this.walletdb.rescan(this.chain.db, 0); -RPC.prototype.importaddress = function importaddress(args, callback) { + return null; +}); + +RPC.prototype.importaddress = function importaddress(args) { if (args.help || args.length < 1 || args.length > 4) { - return callback(new RPCError('importaddress' - + ' "address" ( "label" rescan p2sh )')); + return Promise.reject(new RPCError( + 'importaddress "address" ( "label" rescan p2sh )')); } // Impossible to implement in bcoin. - callback(new Error('Not implemented.')); + return Promise.reject(new Error('Not implemented.')); }; -RPC.prototype.importpubkey = function importpubkey(args, callback) { - var self = this; +RPC.prototype.importpubkey = co(function* importpubkey(args) { var pubkey, label, rescan, key; if (args.help || args.length < 1 || args.length > 4) - return callback(new RPCError('importpubkey "pubkey" ( "label" rescan )')); + throw new RPCError('importpubkey "pubkey" ( "label" rescan )'); pubkey = toString(args[0]); if (!utils.isHex(pubkey)) - return callback(new RPCError('Invalid paremeter.')); + throw new RPCError('Invalid paremeter.'); if (args.length > 1) label = toString(args[1]); @@ -3544,74 +3291,57 @@ RPC.prototype.importpubkey = function importpubkey(args, callback) { rescan = toBool(args[2]); if (rescan && this.chain.db.options.prune) - return callback(new RPCError('Cannot rescan when pruned.')); + throw new RPCError('Cannot rescan when pruned.'); pubkey = new Buffer(pubkey, 'hex'); - key = bcoin.keyring.fromPublic(pubkey, this.network); + key = KeyRing.fromPublic(pubkey, this.network); - this.wallet.importKey(0, key, null, function(err) { - if (err) - return callback(err); + yield this.wallet.importKey(0, key, null); - if (!rescan) - return callback(null, null); + if (!rescan) + return null; - self.walletdb.rescan(self.chain.db, 0, function(err) { - if (err) - return callback(err); - callback(null, null); - }); - }); -}; + yield this.walletdb.rescan(this.chain.db, 0); -RPC.prototype.keypoolrefill = function keypoolrefill(args, callback) { + return null; +}); + +RPC.prototype.keypoolrefill = function keypoolrefill(args) { if (args.help || args.length > 1) - return callback(new RPCError('keypoolrefill ( newsize )')); - callback(null, null); + return Promise.reject(new RPCError('keypoolrefill ( newsize )')); + return Promise.resolve(null); }; -RPC.prototype.listaccounts = function listaccounts(args, callback) { - var self = this; - var map; +RPC.prototype.listaccounts = co(function* listaccounts(args) { + var i, map, accounts, account, balance; if (args.help || args.length > 2) - return callback(new RPCError('listaccounts ( minconf includeWatchonly)')); + throw new RPCError('listaccounts ( minconf includeWatchonly)'); map = {}; + accounts = yield this.wallet.getAccounts(); - this.wallet.getAccounts(function(err, accounts) { - if (err) - return callback(err); + for (i = 0; i < accounts.length; i++) { + account = accounts[i]; + balance = yield this.wallet.getBalance(account); + map[account] = +utils.btc(balance.total); + } - utils.forEachSerial(accounts, function(account, next) { - self.wallet.getBalance(account, function(err, balance) { - if (err) - return next(err); + return map; +}); - map[account] = +utils.btc(balance.total); - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, map); - }); - }); -}; - -RPC.prototype.listaddressgroupings = function listaddressgroupings(args, callback) { +RPC.prototype.listaddressgroupings = function listaddressgroupings(args) { if (args.help) - return callback(new RPCError('listaddressgroupings')); - callback(new Error('Not implemented.')); + return Promise.reject(new RPCError('listaddressgroupings')); + return Promise.resolve(new Error('Not implemented.')); }; -RPC.prototype.listlockunspent = function listlockunspent(args, callback) { +RPC.prototype.listlockunspent = function listlockunspent(args) { var i, outpoints, outpoint, out; if (args.help || args.length > 0) - return callback(new RPCError('listlockunspent')); + return Promise.reject(new RPCError('listlockunspent')); outpoints = this.wallet.tx.getLocked(); out = []; @@ -3624,16 +3354,16 @@ RPC.prototype.listlockunspent = function listlockunspent(args, callback) { }); } - callback(null, out); + return Promise.resolve(out); }; -RPC.prototype.listreceivedbyaccount = function listreceivedbyaccount(args, callback) { +RPC.prototype.listreceivedbyaccount = function listreceivedbyaccount(args) { var minconf = 0; var includeEmpty = false; if (args.help || args.length > 3) { - return callback(new RPCError('listreceivedbyaccount' - + ' ( minconf includeempty includeWatchonly)')); + return Promise.reject(new RPCError( + 'listreceivedbyaccount ( minconf includeempty includeWatchonly )')); } if (args.length > 0) @@ -3642,16 +3372,16 @@ RPC.prototype.listreceivedbyaccount = function listreceivedbyaccount(args, callb if (args.length > 1) includeEmpty = toBool(args[1], false); - this._listReceived(minconf, includeEmpty, true, callback); + return this._listReceived(minconf, includeEmpty, true); }; -RPC.prototype.listreceivedbyaddress = function listreceivedbyaddress(args, callback) { +RPC.prototype.listreceivedbyaddress = function listreceivedbyaddress(args) { var minconf = 0; var includeEmpty = false; if (args.help || args.length > 3) { - return callback(new RPCError('listreceivedbyaddress' - + ' ( minconf includeempty includeWatchonly)')); + return Promise.reject(new RPCError( + 'listreceivedbyaddress ( minconf includeempty includeWatchonly )')); } if (args.length > 0) @@ -3660,121 +3390,114 @@ RPC.prototype.listreceivedbyaddress = function listreceivedbyaddress(args, callb if (args.length > 1) includeEmpty = toBool(args[1], false); - this._listReceived(minconf, includeEmpty, false, callback); + return this._listReceived(minconf, includeEmpty, false); }; -RPC.prototype._listReceived = function _listReceived(minconf, empty, account, callback) { - var self = this; +RPC.prototype._listReceived = co(function* _listReceived(minconf, empty, account) { var out = []; var result = []; var map = {}; var i, j, path, tx, output, conf, hash; - var entry, address, keys, key, item; + var entry, address, keys, key, item, paths, txs; - this.wallet.getPaths(function(err, paths) { - if (err) - return callback(err); + paths = yield this.wallet.getPaths(); - for (i = 0; i < paths.length; i++) { - path = paths[i]; - map[path.hash] = { - involvesWatchonly: false, - address: path.toAddress().toBase58(self.network), - account: path.name, - amount: 0, - confirmations: -1, - label: '', - }; + for (i = 0; i < paths.length; i++) { + path = paths[i]; + map[path.hash] = { + involvesWatchonly: false, + address: path.toAddress().toBase58(this.network), + account: path.name, + amount: 0, + confirmations: -1, + label: '', + }; + } + + txs = yield this.wallet.getHistory(); + + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + + if (minconf) { + if (tx.height === -1) + continue; + if (!(this.chain.height - tx.height + 1 >= minconf)) + continue; } - self.wallet.getHistory(function(err, txs) { - if (err) - return callback(err); + conf = tx.getConfirmations(this.chain.height); - for (i = 0; i < txs.length; i++) { - tx = txs[i]; - - if (minconf) { - if (tx.height === -1) - continue; - if (!(self.chain.height - tx.height + 1 >= minconf)) - continue; - } - - conf = tx.getConfirmations(self.chain.height); - - for (j = 0; j < tx.outputs.length; j++) { - output = tx.outputs[j]; - address = output.getAddress(); - if (!address) - continue; - hash = address.getHash('hex'); - entry = map[hash]; - if (entry) { - if (entry.confirmations === -1 || conf < entry.confirmations) - entry.confirmations = conf; - entry.address = address.toBase58(self.network); - entry.amount += output.value; - } - } + for (j = 0; j < tx.outputs.length; j++) { + output = tx.outputs[j]; + address = output.getAddress(); + if (!address) + continue; + hash = address.getHash('hex'); + entry = map[hash]; + if (entry) { + if (entry.confirmations === -1 || conf < entry.confirmations) + entry.confirmations = conf; + entry.address = address.toBase58(this.network); + entry.amount += output.value; } + } + } - keys = Object.keys(map); - for (i = 0; i < keys.length; i++) { - key = keys[i]; - entry = map[key]; - out.push(entry); + keys = Object.keys(map); + for (i = 0; i < keys.length; i++) { + key = keys[i]; + entry = map[key]; + out.push(entry); + } + + if (account) { + map = {}; + for (i = 0; i < out.length; i++) { + entry = out[i]; + item = map[entry.account]; + if (!item) { + map[entry.account] = entry; + entry.address = undefined; + continue; } + item.amount += entry.amount; + } + out = []; + keys = Object.keys(map); + for (i = 0; i < keys.length; i++) { + key = keys[i]; + entry = map[key]; + out.push(entry); + } + } - if (account) { - map = {}; - for (i = 0; i < out.length; i++) { - entry = out[i]; - item = map[entry.account]; - if (!item) { - map[entry.account] = entry; - entry.address = undefined; - continue; - } - item.amount += entry.amount; - } - out = []; - keys = Object.keys(map); - for (i = 0; i < keys.length; i++) { - key = keys[i]; - entry = map[key]; - out.push(entry); - } - } + for (i = 0; i < out.length; i++) { + entry = out[i]; + if (!empty && entry.amount === 0) + continue; + if (entry.confirmations === -1) + entry.confirmations = 0; + entry.amount = +utils.btc(entry.amount); + result.push(entry); + } - for (i = 0; i < out.length; i++) { - entry = out[i]; - if (!empty && entry.amount === 0) - continue; - if (entry.confirmations === -1) - entry.confirmations = 0; - entry.amount = +utils.btc(entry.amount); - result.push(entry); - } + return result; +}); - callback(null, result); - }); - }); -}; - -RPC.prototype.listsinceblock = function listsinceblock(args, callback) { - var self = this; +RPC.prototype.listsinceblock = co(function* listsinceblock(args) { var block, conf, out, highest; + var i, height, txs, tx, json; if (args.help) { - return callback(new RPCError('listsinceblock' - + ' ( "blockhash" target-confirmations includeWatchonly)')); + throw new RPCError('listsinceblock' + + ' ( "blockhash" target-confirmations includeWatchonly)'); } if (args.length > 0) { block = toHash(args[0]); if (!block) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); } conf = 0; @@ -3784,136 +3507,122 @@ RPC.prototype.listsinceblock = function listsinceblock(args, callback) { out = []; - this.chain.db.getHeight(block, function(err, height) { - if (err) - return callback(err); + height = yield this.chain.db.getHeight(block); - if (height === -1) - height = self.chain.height; + if (height === -1) + height = this.chain.height; - self.wallet.getHistory(function(err, txs) { - if (err) - return callback(err); + txs = yield this.wallet.getHistory(); - utils.forEachSerial(txs, function(tx, next, i) { - if (tx.height < height) - return next(); + for (i = 0; i < txs.length; i++) { + tx = txs[i]; - if (tx.getConfirmations(self.chain.height) < conf) - return next(); + if (tx.height < height) + continue; - if (!highest || tx.height > highest) - highest = tx; + if (tx.getConfirmations(this.chain.height) < conf) + continue; - self._toListTX(tx, function(err, json) { - if (err) - return next(err); - out.push(json); - next(); - }); - }, function(err) { - if (err) - return callback(err); + if (!highest || tx.height > highest) + highest = tx; - callback(null, { - transactions: out, - lastblock: highest && highest.block - ? utils.revHex(highest.block) - : constants.NULL_HASH - }); - }); - }); - }); -}; + json = yield this._toListTX(tx); -RPC.prototype._toListTX = function _toListTX(tx, callback) { - var self = this; + out.push(json); + } + + return { + transactions: out, + lastblock: highest && highest.block + ? utils.revHex(highest.block) + : constants.NULL_HASH + }; +}); + +RPC.prototype._toListTX = co(function* _toListTX(tx) { var i, receive, member, det, sent, received, index; var sendMember, recMember, sendIndex, recIndex, json; + var details; - this.wallet.toDetails(tx, function(err, details) { - if (err) - return callback(err); + details = yield this.wallet.toDetails(tx); - if (!details) - return callback(new RPCError('TX not found.')); + if (!details) + throw new RPCError('TX not found.'); - det = []; - sent = 0; - received = 0; - receive = true; + det = []; + sent = 0; + received = 0; + receive = true; - for (i = 0; i < details.inputs.length; i++) { - member = details.inputs[i]; - if (member.path) { - receive = false; - break; - } + for (i = 0; i < details.inputs.length; i++) { + member = details.inputs[i]; + if (member.path) { + receive = false; + break; } + } - for (i = 0; i < details.outputs.length; i++) { - member = details.outputs[i]; + for (i = 0; i < details.outputs.length; i++) { + member = details.outputs[i]; - if (member.path) { - if (member.path.change === 1) - continue; - received += member.value; - recMember = member; - recIndex = i; + if (member.path) { + if (member.path.branch === 1) continue; - } - - sent += member.value; - sendMember = member; - sendIndex = i; + received += member.value; + recMember = member; + recIndex = i; + continue; } - if (receive) { - member = recMember; - index = recIndex; - } else { - member = sendMember; - index = sendIndex; - } + sent += member.value; + sendMember = member; + sendIndex = i; + } - // In the odd case where we send to ourselves. - if (!member) { - assert(!receive); - member = recMember; - index = recIndex; - } + if (receive) { + member = recMember; + index = recIndex; + } else { + member = sendMember; + index = sendIndex; + } - json = { - account: member.path ? member.path.name : '', - address: member.address - ? member.address.toBase58(self.network) - : null, - category: receive ? 'receive' : 'send', - amount: +utils.btc(receive ? received : -sent), - label: member.path ? member.path.name : undefined, - vout: index, - confirmations: details.confirmations, - blockhash: details.block ? utils.revHex(details.block) : null, - blockindex: details.index, - blocktime: details.ts, - txid: utils.revHex(details.hash), - walletconflicts: [], - time: details.ps, - timereceived: details.ps, - 'bip125-replaceable': 'no' - }; + // In the odd case where we send to ourselves. + if (!member) { + assert(!receive); + member = recMember; + index = recIndex; + } - callback(null, json); - }); -}; + json = { + account: member.path ? member.path.name : '', + address: member.address + ? member.address.toBase58(this.network) + : null, + category: receive ? 'receive' : 'send', + amount: +utils.btc(receive ? received : -sent), + label: member.path ? member.path.name : undefined, + vout: index, + confirmations: details.confirmations, + blockhash: details.block ? utils.revHex(details.block) : null, + blockindex: details.index, + blocktime: details.ts, + txid: utils.revHex(details.hash), + walletconflicts: [], + time: details.ps, + timereceived: details.ps, + 'bip125-replaceable': 'no' + }; -RPC.prototype.listtransactions = function listtransactions(args, callback) { - var self = this; - var account, count; + return json; +}); + +RPC.prototype.listtransactions = co(function* listtransactions(args) { + var i, account, count, txs, tx, json; if (args.help || args.length > 4) { - return callback(new RPCError('listtransactions' - + ' ( "account" count from includeWatchonly)')); + throw new RPCError( + 'listtransactions ( "account" count from includeWatchonly)'); } account = null; @@ -3932,35 +3641,26 @@ RPC.prototype.listtransactions = function listtransactions(args, callback) { if (count < 0) count = 10; - this.wallet.getHistory(account, function(err, txs) { - if (err) - return callback(err); + txs = yield this.wallet.getHistory(); - utils.forEachSerial(txs, function(tx, next, i) { - self._toListTX(tx, function(err, json) { - if (err) - return next(err); - txs[i] = json; - next(); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, txs); - }); - }); -}; + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + json = yield this._toListTX(tx); + txs[i] = json; + } -RPC.prototype.listunspent = function listunspent(args, callback) { - var self = this; + return txs; +}); + +RPC.prototype.listunspent = co(function* listunspent(args) { var minDepth = 1; var maxDepth = 9999999; var out = []; - var i, addresses, addrs, depth, address, hash; + var i, addresses, addrs, depth, address, hash, coins, coin, ring; if (args.help || args.length > 3) { - return callback(new RPCError('listunspent' - + ' ( minconf maxconf ["address",...] )')); + throw new RPCError('listunspent' + + ' ( minconf maxconf ["address",...] )'); } if (args.length > 0) @@ -3976,76 +3676,68 @@ RPC.prototype.listunspent = function listunspent(args, callback) { addresses = {}; for (i = 0; i < addrs.length; i++) { address = toString(addrs[i]); - hash = bcoin.address.getHash(address, 'hex'); + hash = Address.getHash(address, 'hex'); if (!hash) - return callback(new RPCError('Invalid address.')); + throw new RPCError('Invalid address.'); if (addresses[hash]) - return callback(new RPCError('Duplicate address.')); + throw new RPCError('Duplicate address.'); addresses[hash] = true; } } - this.wallet.getCoins(function(err, coins) { - if (err) - return callback(err); + coins = yield this.wallet.getCoins(); - utils.forEachSerial(coins, function(coin, next) { - depth = coin.height !== -1 - ? self.chain.height - coin.height + 1 - : 0; + for (i = 0; i < coins.length; i++ ) { + coin = coins[i]; - if (!(depth >= minDepth && depth <= maxDepth)) - return next(); + depth = coin.height !== -1 + ? this.chain.height - coin.height + 1 + : 0; - address = coin.getAddress(); + if (!(depth >= minDepth && depth <= maxDepth)) + continue; - if (!address) - return next(); + address = coin.getAddress(); - hash = coin.getHash('hex'); + if (!address) + continue; - if (addresses) { - if (!hash || !addresses[hash]) - return next(); - } + hash = coin.getHash('hex'); - self.wallet.getKeyRing(hash, function(err, ring) { - if (err) - return next(err); + if (addresses) { + if (!hash || !addresses[hash]) + continue; + } - out.push({ - txid: utils.revHex(coin.hash), - vout: coin.index, - address: address ? address.toBase58(self.network) : null, - account: ring ? ring.path.name : undefined, - redeemScript: ring && ring.script - ? ring.script.toJSON() - : undefined, - scriptPubKey: coin.script.toJSON(), - amount: +utils.btc(coin.value), - confirmations: depth, - spendable: !self.wallet.tx.isLocked(coin), - solvable: true - }); + ring = yield this.wallet.getKey(hash); - next(); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, out); + out.push({ + txid: utils.revHex(coin.hash), + vout: coin.index, + address: address ? address.toBase58(this.network) : null, + account: ring ? ring.name : undefined, + redeemScript: ring && ring.script + ? ring.script.toJSON() + : undefined, + scriptPubKey: coin.script.toJSON(), + amount: +utils.btc(coin.value), + confirmations: depth, + spendable: !this.wallet.tx.isLocked(coin), + solvable: true }); - }); -}; + } -RPC.prototype.lockunspent = function lockunspent(args, callback) { + return out; +}); + +RPC.prototype.lockunspent = function lockunspent(args) { var i, unlock, outputs, output, outpoint; if (args.help || args.length < 1 || args.length > 2) { - return callback(new RPCError('lockunspent' + return Promise.reject(new RPCError('lockunspent' + ' unlock ([{"txid":"txid","vout":n},...])')); } @@ -4054,29 +3746,29 @@ RPC.prototype.lockunspent = function lockunspent(args, callback) { if (args.length === 1) { if (unlock) this.wallet.tx.unlockCoins(); - return callback(null, true); + return Promise.resolve(true); } outputs = toArray(args[1]); if (!outputs) - return callback(new RPCError('Invalid paremeter.')); + return Promise.reject(new RPCError('Invalid paremeter.')); for (i = 0; i < outputs.length; i++) { output = outputs[i]; if (!output || typeof output !== 'object') - return callback(new RPCError('Invalid paremeter.')); + return Promise.reject(new RPCError('Invalid paremeter.')); - outpoint = new bcoin.outpoint(); + outpoint = new Outpoint(); outpoint.hash = toHash(output.txid); outpoint.index = toNumber(output.vout); if (!outpoint.txid) - return callback(new RPCError('Invalid paremeter.')); + return Promise.reject(new RPCError('Invalid paremeter.')); if (outpoint.index < 0) - return callback(new RPCError('Invalid paremeter.')); + return Promise.reject(new RPCError('Invalid paremeter.')); if (unlock) this.wallet.unlockCoin(outpoint); @@ -4084,16 +3776,18 @@ RPC.prototype.lockunspent = function lockunspent(args, callback) { this.wallet.lockCoin(outpoint); } - callback(null, true); + return Promise.resolve(true); }; -RPC.prototype.move = function move(args, callback) { +RPC.prototype.move = function move(args) { // Not implementing: stupid and deprecated. - callback(new Error('Not implemented.')); + Promise.reject(new Error('Not implemented.')); }; -RPC.prototype._send = function _send(account, address, amount, subtractFee, callback) { - var options = { +RPC.prototype._send = co(function* _send(account, address, amount, subtractFee) { + var tx, options; + + options = { account: account, subtractFee: subtractFee, rate: this.feeRate, @@ -4103,39 +3797,37 @@ RPC.prototype._send = function _send(account, address, amount, subtractFee, call }] }; - this.wallet.send(options, callback); -}; + tx = yield this.wallet.send(options); -RPC.prototype.sendfrom = function sendfrom(args, callback) { + return tx.rhash; +}); + +RPC.prototype.sendfrom = function sendfrom(args) { var account, address, amount; if (args.help || args.length < 3 || args.length > 6) { - return callback(new RPCError('sendfrom' + return Promise.reject(new RPCError('sendfrom' + ' "fromaccount" "tobitcoinaddress"' + ' amount ( minconf "comment" "comment-to" )')); } account = toString(args[0]); - address = bcoin.address.fromBase58(toString(args[1])); + address = Address.fromBase58(toString(args[1])); amount = toSatoshi(args[2]); if (!account) account = 'default'; - this._send(account, address, amount, false, function(err, tx) { - if (err) - return callback(err); - callback(null, tx.rhash); - }); + return this._send(account, address, amount, false); }; -RPC.prototype.sendmany = function sendmany(args, callback) { +RPC.prototype.sendmany = co(function* sendmany(args) { var account, sendTo, minDepth, comment, subtractFee; - var i, outputs, keys, uniq; + var i, outputs, keys, uniq, tx; var key, value, address, hash, output, options; if (args.help || args.length < 2 || args.length > 5) { - return callback(new RPCError('sendmany' + return Promise.reject(new RPCError('sendmany' + ' "fromaccount" {"address":amount,...}' + ' ( minconf "comment" ["address",...] )')); } @@ -4148,7 +3840,7 @@ RPC.prototype.sendmany = function sendmany(args, callback) { account = 'default'; if (!sendTo) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); if (args.length > 2) minDepth = toNumber(args[2], 1); @@ -4166,15 +3858,15 @@ RPC.prototype.sendmany = function sendmany(args, callback) { for (i = 0; i < keys.length; i++) { key = keys[i]; value = toSatoshi(sendTo[key]); - address = bcoin.address.fromBase58(key); + address = Address.fromBase58(key); hash = address.getHash('hex'); if (uniq[hash]) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); uniq[hash] = true; - output = new bcoin.output(); + output = new Output(); output.value = value; output.script.fromAddress(address); outputs.push(output); @@ -4187,227 +3879,205 @@ RPC.prototype.sendmany = function sendmany(args, callback) { confirmations: minDepth }; - this.wallet.send(options, function(err, tx) { - if (err) - return callback(err); - callback(null, tx.rhash); - }); -}; + tx = yield this.wallet.send(options); -RPC.prototype.sendtoaddress = function sendtoaddress(args, callback) { + return tx.rhash; +}); + +RPC.prototype.sendtoaddress = function sendtoaddress(args) { var address, amount, subtractFee; if (args.help || args.length < 2 || args.length > 5) { - return callback(new RPCError('sendtoaddress' + return Promise.reject(new RPCError('sendtoaddress' + ' "bitcoinaddress" amount' + ' ( "comment" "comment-to"' + ' subtractfeefromamount )')); } - address = bcoin.address.fromBase58(toString(args[0])); + address = Address.fromBase58(toString(args[0])); amount = toSatoshi(args[1]); subtractFee = toBool(args[4]); - this._send(null, address, amount, subtractFee, function(err, tx) { - if (err) - return callback(err); - callback(null, tx.rhash); - }); + return this._send(null, address, amount, subtractFee); }; -RPC.prototype.setaccount = function setaccount(args, callback) { - if (args.help || args.length < 1 || args.length > 2) - return callback(new RPCError('setaccount "bitcoinaddress" "account"')); +RPC.prototype.setaccount = function setaccount(args) { + if (args.help || args.length < 1 || args.length > 2) { + return Promise.reject(new RPCError( + 'setaccount "bitcoinaddress" "account"')); + } // Impossible to implement in bcoin: - callback(new Error('Not implemented.')); + return Promise.reject(new Error('Not implemented.')); }; -RPC.prototype.settxfee = function settxfee(args, callback) { +RPC.prototype.settxfee = function settxfee(args) { if (args.help || args.length < 1 || args.length > 1) - return callback(new RPCError('settxfee amount')); + return Promise.reject(new RPCError('settxfee amount')); this.feeRate = toSatoshi(args[0]); - callback(null, true); + return Promise.resolve(true); }; -RPC.prototype.signmessage = function signmessage(args, callback) { - var self = this; - var address, msg, sig; +RPC.prototype.signmessage = co(function* signmessage(args) { + var address, msg, sig, ring; if (args.help || args.length !== 2) - return callback(new RPCError('signmessage "bitcoinaddress" "message"')); + throw new RPCError('signmessage "bitcoinaddress" "message"'); address = toString(args[0]); msg = toString(args[1]); - address = bcoin.address.getHash(address, 'hex'); + address = Address.getHash(address, 'hex'); if (!address) - return callback(new RPCError('Invalid address.')); + throw new RPCError('Invalid address.'); - this.wallet.getKeyRing(address, function(err, ring) { - if (err) - return callback(err); + ring = yield this.wallet.getKey(address); - if (!ring) - return callback(new RPCError('Address not found.')); + if (!ring) + throw new RPCError('Address not found.'); - if (!self.wallet.master.key) - return callback(new RPCError('Wallet is locked.')); + if (!this.wallet.master.key) + throw new RPCError('Wallet is locked.'); - msg = new Buffer(RPC.magic + msg, 'utf8'); - msg = crypto.hash256(msg); + msg = new Buffer(RPC.magic + msg, 'utf8'); + msg = crypto.hash256(msg); - sig = ring.sign(msg); + sig = ring.sign(msg); - callback(null, sig.toString('base64')); - }); -}; + return sig.toString('base64'); +}); -RPC.prototype.walletlock = function walletlock(args, callback) { +RPC.prototype.walletlock = co(function* walletlock(args) { if (args.help || (this.wallet.master.encrypted && args.length !== 0)) - return callback(new RPCError('walletlock')); + throw new RPCError('walletlock'); if (!this.wallet.master.encrypted) - return callback(new RPCError('Wallet is not encrypted.')); + throw new RPCError('Wallet is not encrypted.'); - this.wallet.lock(); - callback(null, null); -}; + yield this.wallet.lock(); -RPC.prototype.walletpassphrasechange = function walletpassphrasechange(args, callback) { + return null; +}); + +RPC.prototype.walletpassphrasechange = co(function* walletpassphrasechange(args) { var old, new_; if (args.help || (this.wallet.master.encrypted && args.length !== 2)) { - return callback(new RPCError('walletpassphrasechange' - + ' "oldpassphrase" "newpassphrase"')); + throw new RPCError('walletpassphrasechange' + + ' "oldpassphrase" "newpassphrase"'); } if (!this.wallet.master.encrypted) - return callback(new RPCError('Wallet is not encrypted.')); + throw new RPCError('Wallet is not encrypted.'); old = toString(args[0]); new_ = toString(args[1]); if (old.length < 1 || new_.length < 1) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); - this.wallet.setPassphrase(old, new_, function(err) { - if (err) - return callback(err); + yield this.wallet.setPassphrase(old, new_); - callback(null, null); - }); -}; + return null; +}); -RPC.prototype.walletpassphrase = function walletpassphrase(args, callback) { +RPC.prototype.walletpassphrase = co(function* walletpassphrase(args) { var passphrase, timeout; if (args.help || (this.wallet.master.encrypted && args.length !== 2)) - return callback(new RPCError('walletpassphrase "passphrase" timeout')); + throw new RPCError('walletpassphrase "passphrase" timeout'); if (!this.wallet.master.encrypted) - return callback(new RPCError('Wallet is not encrypted.')); + throw new RPCError('Wallet is not encrypted.'); passphrase = toString(args[0]); timeout = toNumber(args[1]); if (passphrase.length < 1) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); if (timeout < 0) - return callback(new RPCError('Invalid parameter')); + throw new RPCError('Invalid parameter'); - this.wallet.unlock(passphrase, timeout, function(err) { - if (err) - return callback(err); - callback(null, null); - }); -}; + yield this.wallet.unlock(passphrase, timeout); -RPC.prototype.importprunedfunds = function importprunedfunds(args, callback) { - var self = this; - var tx, block, label; + return null; +}); + +RPC.prototype.importprunedfunds = co(function* importprunedfunds(args) { + var tx, block, label, height, added; if (args.help || args.length < 2 || args.length > 3) { - return callback(new RPCError('importprunedfunds' - + ' "rawtransaction" "txoutproof" ( "label" )')); + throw new RPCError('importprunedfunds' + + ' "rawtransaction" "txoutproof" ( "label" )'); } tx = args[0]; block = args[1]; if (!utils.isHex(tx) || !utils.isHex(block)) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); - tx = bcoin.tx.fromRaw(tx, 'hex'); - block = bcoin.merkleblock.fromRaw(block, 'hex'); + tx = TX.fromRaw(tx, 'hex'); + block = MerkleBlock.fromRaw(block, 'hex'); if (args.length === 3) label = toString(args[2]); if (!block.verify()) - return callback(new RPCError('Invalid proof.')); + throw new RPCError('Invalid proof.'); if (!block.hasTX(tx)) - return callback(new RPCError('Invalid proof.')); + throw new RPCError('Invalid proof.'); - this.chain.db.getHeight(block.hash('hex'), function(err, height) { - if (err) - return callback(err); + height = yield this.chain.db.getHeight(block.hash('hex')); - if (height === -1) - return callback(new RPCError('Invalid proof.')); + if (height === -1) + throw new RPCError('Invalid proof.'); - tx.index = block.indexOf(tx); - tx.block = block.hash('hex'); - tx.ts = block.ts; - tx.height = height; + tx.index = block.indexOf(tx); + tx.block = block.hash('hex'); + tx.ts = block.ts; + tx.height = height; - self.wallet.addTX(tx, function(err, added) { - if (err) - return callback(err); + added = yield this.wallet.addTX(tx); - if (!added) - return callback(new RPCError('No tracked address for TX.')); + if (!added) + throw new RPCError('No tracked address for TX.'); - callback(null, null); - }); - }); -}; + return null; +}); -RPC.prototype.removeprunedfunds = function removeprunedfunds(args, callback) { - var hash; +RPC.prototype.removeprunedfunds = co(function* removeprunedfunds(args) { + var hash, removed; if (args.help || args.length !== 1) - return callback(new RPCError('removeprunedfunds "txid"')); + throw new RPCError('removeprunedfunds "txid"'); hash = toHash(args[0]); if (!hash) - return callback(new RPCError('Invalid parameter.')); + throw new RPCError('Invalid parameter.'); - this.wallet.tx.remove(hash, function(err, removed) { - if (err) - return callback(err); + removed = yield this.wallet.tx.remove(hash); - if (!removed) - return callback(new RPCError('Transaction not in wallet.')); + if (!removed) + throw new RPCError('Transaction not in wallet.'); - callback(null, null); - }); -}; + return null; +}); -RPC.prototype.getmemory = function getmemory(args, callback) { +RPC.prototype.getmemory = function getmemory(args) { var mem; if (args.help || args.length !== 0) - return callback(new RPCError('getmemory')); + return Promise.reject(new RPCError('getmemory')); mem = process.memoryUsage(); - callback(null, { + return Promise.resolve({ rss: utils.mb(mem.rss), jsheap: utils.mb(mem.heapUsed), jsheaptotal: utils.mb(mem.heapTotal), @@ -4485,6 +4155,18 @@ function reverseEndian(data) { } } +function writeFile(file, data) { + return new Promise(function(resolve, reject) { + fs.writeFile(file, data, co.wrap(resolve, reject)); + }); +} + +function readFile(file, enc) { + return new Promise(function(resolve, reject) { + fs.readFile(file, enc, co.wrap(resolve, reject)); + }); +} + /* * Expose */ diff --git a/lib/http/rpcclient.js b/lib/http/rpcclient.js index dd1b2688..16fa6222 100644 --- a/lib/http/rpcclient.js +++ b/lib/http/rpcclient.js @@ -8,6 +8,7 @@ var Network = require('../protocol/network'); var request = require('./request'); +var co = require('../utils/co'); /** * BCoin RPC client. @@ -40,11 +41,11 @@ function RPCClient(options) { * @private * @param {String} method - RPC method name. * @param {Array} params - RPC parameters. - * @param {Function} callback - Returns [Error, Object?]. + * @returns {Promise} - Returns Object?. */ -RPCClient.prototype.call = function call(method, params, callback) { - request({ +RPCClient.prototype.call = co(function* call(method, params) { + var res = yield request.promise({ method: 'POST', uri: this.uri, json: { @@ -57,25 +58,22 @@ RPCClient.prototype.call = function call(method, params, callback) { password: this.apiKey || '' }, expect: 'json' - }, function(err, res, body) { - if (err) - return callback(err); - - if (!body) - return callback(); - - if (res.statusCode === 400) - return callback(null, body.result); - - if (res.statusCode !== 200) { - if (body.error) - return callback(new Error(body.error.message)); - return callback(new Error('Status code: ' + res.statusCode)); - } - - return callback(null, body.result); }); -}; + + if (!res.body) + return; + + if (res.statusCode === 400) + return res.body.result; + + if (res.statusCode !== 200) { + if (res.body.error) + throw new Error(res.body.error.message); + throw new Error('Status code: ' + res.statusCode); + } + + return res.body.result; +}); /* * Expose diff --git a/lib/http/server.js b/lib/http/server.js index fa702f67..6497d47a 100644 --- a/lib/http/server.js +++ b/lib/http/server.js @@ -9,15 +9,18 @@ /* jshint -W069 */ -var bcoin = require('../env'); var EventEmitter = require('events').EventEmitter; -var constants = bcoin.constants; -var http = require('./'); -var HTTPBase = http.base; +var constants = require('../protocol/constants'); +var HTTPBase = require('./base'); var utils = require('../utils/utils'); +var co = require('../utils/co'); +var Address = require('../primitives/address'); +var TX = require('../primitives/tx'); +var Script = require('../script/script'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var RPC; /*= require('./rpc'); - load lazily */ +var assert = require('assert'); +var con = co.con; +var RPC; /** * HTTPServer @@ -90,6 +93,7 @@ HTTPServer.prototype._init = function _init() { this.server.on('request', function(req, res) { if (req.pathname === '/') return; + self.logger.debug('Request for path=%s (%s).', req.pathname, req.socket.remoteAddress); }); @@ -99,7 +103,7 @@ HTTPServer.prototype._init = function _init() { address.address, address.port); }); - this.use(function(req, res, next, send) { + this.use(function(req, res, send, next) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader( @@ -113,14 +117,14 @@ HTTPServer.prototype._init = function _init() { res.setHeader('X-Bcoin-Version', constants.USER_VERSION); res.setHeader('X-Bcoin-Agent', constants.USER_AGENT); - res.setHeader('X-Bcoin-Network', self.network.type); - res.setHeader('X-Bcoin-Height', self.chain.height + ''); - res.setHeader('X-Bcoin-Tip', utils.revHex(self.chain.tip.hash)); + res.setHeader('X-Bcoin-Network', this.network.type); + res.setHeader('X-Bcoin-Height', this.chain.height + ''); + res.setHeader('X-Bcoin-Tip', utils.revHex(this.chain.tip.hash)); next(); }); - this.use(function(req, res, next, send) { + this.use(function(req, res, send, next) { var auth = req.headers['authorization']; var parts; @@ -143,11 +147,11 @@ HTTPServer.prototype._init = function _init() { next(); }); - this.use(function(req, res, next, send) { - if (!self.apiHash) + this.use(function(req, res, send, next) { + if (!this.apiHash) return next(); - if (crypto.ccmp(hash256(req.password), self.apiHash)) + if (crypto.ccmp(hash256(req.password), this.apiHash)) return next(); res.setHeader('WWW-Authenticate', 'Basic realm="node"'); @@ -168,7 +172,7 @@ HTTPServer.prototype._init = function _init() { send(401, { error: 'Bad API key.' }); }); - this.use(function(req, res, next, send) { + this.use(function(req, res, send, next) { var i, params, options, output, address; if (req.method === 'POST' && req.pathname === '/') { @@ -185,8 +189,8 @@ HTTPServer.prototype._init = function _init() { softMerge(params, req.query, true); softMerge(params, req.body); - self.logger.debug('Params:'); - self.logger.debug(params); + this.logger.debug('Params:'); + this.logger.debug(params); if (params.id) { assert(typeof params.id === 'string', 'ID must be a string.'); @@ -282,6 +286,8 @@ HTTPServer.prototype._init = function _init() { for (i = 0; i < params.outputs.length; i++) { output = params.outputs[i]; + assert(output && typeof output === 'object', 'Output must be an object.'); + if (output.address) assert(typeof output.address === 'string', 'Address must be a string.'); else if (output.script) @@ -291,10 +297,10 @@ HTTPServer.prototype._init = function _init() { options.outputs.push({ address: output.address - ? bcoin.address.fromBase58(output.address) + ? Address.fromBase58(output.address) : null, script: output.script - ? bcoin.script.fromRaw(output.script, 'hex') + ? Script.fromRaw(output.script, 'hex') : null, value: utils.satoshi(output.value) }); @@ -307,20 +313,20 @@ HTTPServer.prototype._init = function _init() { for (i = 0; i < params.address.length; i++) { address = params.address[i]; assert(typeof address === 'string', 'Address must be a string.'); - address = bcoin.address.fromBase58(address); + address = Address.fromBase58(address); } } else { assert(typeof params.address === 'string', 'Address must be a string.'); - options.address = bcoin.address.fromBase58(params.address); + options.address = Address.fromBase58(params.address); } } if (params.tx) { if (typeof params.tx === 'object') { - options.tx = bcoin.tx.fromJSON(params.tx); + options.tx = TX.fromJSON(params.tx); } else { assert(typeof params.tx === 'string', 'TX must be a hex string.'); - options.tx = bcoin.tx.fromRaw(params.tx, 'hex'); + options.tx = TX.fromRaw(params.tx, 'hex'); } } @@ -362,8 +368,8 @@ HTTPServer.prototype._init = function _init() { } if (params.token) { - assert(utils.isHex(params.token), 'API key must be a hex string.'); - assert(params.token.length === 64, 'API key must be 32 bytes.'); + assert(utils.isHex(params.token), 'Wallet token must be a hex string.'); + assert(params.token.length === 64, 'Wallet token must be 32 bytes.'); options.token = new Buffer(params.token, 'hex'); } @@ -372,75 +378,54 @@ HTTPServer.prototype._init = function _init() { next(); }); - this.use(function(req, res, next, send) { - if (req.path.length < 2 || req.path[0] !== 'wallet') - return next(); + this.use(con(function* (req, res, send, next) { + var wallet; - if (!self.options.walletAuth) { - return self.walletdb.get(req.options.id, function(err, wallet) { - if (err) - return next(err); - - if (!wallet) - return send(404); - - req.wallet = wallet; - - return next(); - }); + if (req.path.length < 2 || req.path[0] !== 'wallet') { + next(); + return; } - self.walletdb.auth(req.options.id, req.options.token, function(err, wallet) { - if (err) { - self.logger.info('Auth failure for %s: %s.', - req.options.id, err.message); - send(403, { error: err.message }); + if (!this.options.walletAuth) { + wallet = yield this.walletdb.get(req.options.id); + + if (!wallet) { + send(404); return; } - if (!wallet) - return send(404); - req.wallet = wallet; - self.logger.info('Successful auth for %s.', req.options.id); - next(); - }); - }); - // JSON RPC - this.post('/', function(req, res, next, send) { - if (!self.rpc) { - RPC = require('./rpc'); - self.rpc = new RPC(self.node); + next(); + return; } - function handle(err, json) { - if (err) { - self.logger.error(err); + try { + wallet = yield this.walletdb.auth(req.options.id, req.options.token); + } catch (err) { + this.logger.info('Auth failure for %s: %s.', + req.options.id, err.message); + send(403, { error: err.message }); + return; + } - if (err.type === 'RPCError') { - return send(400, { - result: err.message, - error: null, - id: req.body.id - }); - } + if (!wallet) { + send(404); + return; + } - return send(500, { - result: null, - error: { - message: err.message, - code: 1 - }, - id: req.body.id - }); - } + req.wallet = wallet; + this.logger.info('Successful auth for %s.', req.options.id); + next(); + })); - send(200, { - result: json != null ? json : null, - error: null, - id: req.body.id - }); + // JSON RPC + this.post('/', con(function* (req, res, send, next) { + var json; + + if (!this.rpc) { + RPC = require('./rpc'); + this.rpc = new RPC(this.node); } if (req.body.method === 'getwork') { @@ -450,497 +435,378 @@ HTTPServer.prototype._init = function _init() { } try { - self.rpc.execute(req.body, handle); - } catch (e) { - handle(e); - } - }); + json = yield this.rpc.execute(req.body); + } catch (err) { + this.logger.error(err); - this.get('/', function(req, res, next, send) { + if (err.type === 'RPCError') { + return send(400, { + result: err.message, + error: null, + id: req.body.id + }); + } + + return send(500, { + result: null, + error: { + message: err.message, + code: 1 + }, + id: req.body.id + }); + } + + send(200, { + result: json != null ? json : null, + error: null, + id: req.body.id + }); + })); + + this.get('/', function(req, res, send, next) { send(200, { version: constants.USER_VERSION, agent: constants.USER_AGENT, - services: self.pool.services, - network: self.network.type, - height: self.chain.height, - tip: self.chain.tip.rhash, - peers: self.pool.peers.all.length, - progress: self.chain.getProgress() + services: this.pool.services, + network: this.network.type, + height: this.chain.height, + tip: this.chain.tip.rhash, + peers: this.pool.peers.all.length, + progress: this.chain.getProgress() }); }); // UTXO by address - this.get('/coin/address/:address', function(req, res, next, send) { - self.node.getCoinsByAddress(req.options.address, function(err, coins) { - if (err) - return next(err); - - send(200, coins.map(function(coin) { - return coin.toJSON(); - })); - }); - }); + this.get('/coin/address/:address', con(function* (req, res, send, next) { + var coins = yield this.node.getCoinsByAddress(req.options.address); + send(200, coins.map(function(coin) { + return coin.toJSON(); + })); + })); // UTXO by id - this.get('/coin/:hash/:index', function(req, res, next, send) { - self.node.getCoin(req.options.hash, req.options.index, function(err, coin) { - if (err) - return next(err); + this.get('/coin/:hash/:index', con(function* (req, res, send, next) { + var coin = yield this.node.getCoin(req.options.hash, req.options.index); - if (!coin) - return send(404); + if (!coin) + return send(404); - send(200, coin.toJSON()); - }); - }); + send(200, coin.toJSON()); + })); // Bulk read UTXOs - this.post('/coin/address', function(req, res, next, send) { - self.node.getCoinsByAddress(req.options.address, function(err, coins) { - if (err) - return next(err); - - send(200, coins.map(function(coin) { - return coin.toJSON(); - })); - }); - }); + this.post('/coin/address', con(function* (req, res, send, next) { + var coins = yield this.node.getCoinsByAddress(req.options.address); + send(200, coins.map(function(coin) { + return coin.toJSON(); + })); + })); // TX by hash - this.get('/tx/:hash', function(req, res, next, send) { - self.node.getTX(req.options.hash, function(err, tx) { - if (err) - return next(err); + this.get('/tx/:hash', con(function* (req, res, send, next) { + var tx = yield this.node.getTX(req.options.hash); - if (!tx) - return send(404); + if (!tx) + return send(404); - self.node.fillHistory(tx, function(err) { - if (err) - return next(err); + yield this.node.fillHistory(tx); - send(200, tx.toJSON()); - }); - }); - }); + send(200, tx.toJSON()); + })); // TX by address - this.get('/tx/address/:address', function(req, res, next, send) { - self.node.getTXByAddress(req.options.address, function(err, txs) { - if (err) - return next(err); + this.get('/tx/address/:address', con(function* (req, res, send, next) { + var txs = yield this.node.getTXByAddress(req.options.address); + var i, tx; - utils.forEachSerial(txs, function(tx, next) { - self.node.fillHistory(tx, next); - }, function(err) { - if (err) - return next(err); + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + yield this.node.fillHistory(tx); + } - send(200, txs.map(function(tx) { - return tx.toJSON(); - })); - }); - }); - }); + send(200, txs.map(function(tx) { + return tx.toJSON(); + })); + })); // Bulk read TXs - this.post('/tx/address', function(req, res, next, send) { - self.node.getTXByAddress(req.options.address, function(err, txs) { - if (err) - return next(err); + this.post('/tx/address', con(function* (req, res, send, next) { + var txs = yield this.node.getTXByAddress(req.options.address); + var i, tx; - utils.forEachSerial(txs, function(tx, next) { - self.node.fillHistory(tx, next); - }, function(err) { - if (err) - return next(err); + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + yield this.node.fillHistory(tx); + } - send(200, txs.map(function(tx) { - return tx.toJSON(); - })); - }); - }); - }); + send(200, txs.map(function(tx) { + return tx.toJSON(); + })); + })); // Block by hash/height - this.get('/block/:hash', function(req, res, next, send) { + this.get('/block/:hash', con(function* (req, res, send, next) { var hash = req.options.hash || req.options.height; - self.node.getFullBlock(hash, function(err, block) { - if (err) - return next(err); + var block = yield this.node.getFullBlock(hash); - if (!block) - return send(404); + if (!block) + return send(404); - send(200, block.toJSON()); - }); - }); + send(200, block.toJSON()); + })); // Mempool snapshot - this.get('/mempool', function(req, res, next, send) { - if (!self.mempool) + this.get('/mempool', con(function* (req, res, send, next) { + var i, txs, tx; + + if (!this.mempool) return send(400, { error: 'No mempool available.' }); - self.mempool.getHistory(function(err, txs) { - if (err) - return next(err); + txs = this.mempool.getHistory(); - utils.forEachSerial(txs, function(tx, next) { - self.node.fillHistory(tx, next); - }, function(err) { - if (err) - return next(err); + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + yield this.node.fillHistory(tx); + } - send(200, txs.map(function(tx) { - return tx.toJSON(); - })); - }); - }); - }); + send(200, txs.map(function(tx) { + return tx.toJSON(); + })); + })); // Broadcast TX - this.post('/broadcast', function(req, res, next, send) { - self.node.sendTX(req.options.tx, function(err) { - if (err) - return next(err); - - send(200, { success: true }); - }); - }); + this.post('/broadcast', con(function* (req, res, send, next) { + yield this.node.sendTX(req.options.tx); + send(200, { success: true }); + })); // Estimate fee - this.get('/fee', function(req, res, next, send) { + this.get('/fee', function(req, res, send, next) { var fee; - if (!self.fees) + if (!this.fees) return send(400, { error: 'Fee estimation not available.' }); - fee = self.fees.estimateFee(req.options.blocks); + fee = this.fees.estimateFee(req.options.blocks); send(200, { rate: utils.btc(fee) }); }); // Get wallet - this.get('/wallet/:id', function(req, res, next, send) { + this.get('/wallet/:id', function(req, res, send, next) { send(200, req.wallet.toJSON()); }); // Create wallet - this.post('/wallet/:id?', function(req, res, next, send) { - self.walletdb.create(req.options, function(err, wallet) { - if (err) - return next(err); - - send(200, wallet.toJSON()); - }); - }); + this.post('/wallet/:id?', con(function* (req, res, send, next) { + var wallet = yield this.walletdb.create(req.options); + send(200, wallet.toJSON()); + })); // List accounts - this.get('/wallet/:id/account', function(req, res, next, send) { - req.wallet.getAccounts(function(err, accounts) { - if (err) - return next(err); - - send(200, accounts); - }); - }); + this.get('/wallet/:id/account', con(function* (req, res, send, next) { + var accounts = yield req.wallet.getAccounts(); + send(200, accounts); + })); // Get account - this.get('/wallet/:id/account/:account', function(req, res, next, send) { - req.wallet.getAccount(req.options.account, function(err, account) { - if (err) - return next(err); + this.get('/wallet/:id/account/:account', con(function* (req, res, send, next) { + var account = yield req.wallet.getAccount(req.options.account); - if (!account) - return send(404); + if (!account) + return send(404); - send(200, account.toJSON()); - }); - }); + send(200, account.toJSON()); + })); // Create/get account - this.post('/wallet/:id/account/:account?', function(req, res, next, send) { - req.wallet.createAccount(req.options, function(err, account) { - if (err) - return next(err); + this.post('/wallet/:id/account/:account?', con(function* (req, res, send, next) { + var account = yield req.wallet.createAccount(req.options); - if (!account) - return send(404); + if (!account) + return send(404); - send(200, account.toJSON()); - }); - }); + send(200, account.toJSON()); + })); // Change passphrase - this.post('/wallet/:id/passphrase', function(req, res, next, send) { + this.post('/wallet/:id/passphrase', con(function* (req, res, send, next) { var options = req.options; var old = options.old; var new_ = options.passphrase; - req.wallet.setPassphrase(old, new_, function(err) { - if (err) - return next(err); - - send(200, { success: true }); - }); - }); + yield req.wallet.setPassphrase(old, new_); + send(200, { success: true }); + })); // Generate new token - this.post('/wallet/:id/retoken', function(req, res, next, send) { + this.post('/wallet/:id/retoken', con(function* (req, res, send, next) { var options = req.options; - req.wallet.retoken(options.passphrase, function(err, token) { - if (err) - return next(err); - - send(200, { token: token.toString('hex') }); - }); - }); + var token = yield req.wallet.retoken(options.passphrase); + send(200, { token: token.toString('hex') }); + })); // Send TX - this.post('/wallet/:id/send', function(req, res, next, send) { + this.post('/wallet/:id/send', con(function* (req, res, send, next) { var options = req.options; - - req.wallet.send(options, function(err, tx) { - if (err) - return next(err); - - send(200, tx.toJSON()); - }); - }); + var tx = yield req.wallet.send(options); + send(200, tx.toJSON()); + })); // Create TX - this.post('/wallet/:id/create', function(req, res, next, send) { + this.post('/wallet/:id/create', con(function* (req, res, send, next) { var options = req.options; - - req.wallet.createTX(options, function(err, tx) { - if (err) - return next(err); - - req.wallet.sign(tx, options, function(err) { - if (err) - return next(err); - - send(200, tx.toJSON()); - }); - }); - }); + var tx = yield req.wallet.createTX(options); + yield req.wallet.sign(tx, options); + send(200, tx.toJSON()); + })); // Sign TX - this.post('/wallet/:id/sign', function(req, res, next, send) { + this.post('/wallet/:id/sign', con(function* (req, res, send, next) { var options = req.options; var tx = req.options.tx; - - req.wallet.sign(tx, options, function(err) { - if (err) - return next(err); - - send(200, tx.toJSON()); - }); - }); + yield req.wallet.sign(tx, options); + send(200, tx.toJSON()); + })); // Fill TX - this.post('/wallet/:id/fill', function(req, res, next, send) { + this.post('/wallet/:id/fill', con(function* (req, res, send, next) { var tx = req.options.tx; - - req.wallet.fillHistory(tx, function(err) { - if (err) - return next(err); - - send(200, tx.toJSON()); - }); - }); + yield req.wallet.fillHistory(tx); + send(200, tx.toJSON()); + })); // Zap Wallet TXs - this.post('/wallet/:id/zap', function(req, res, next, send) { + this.post('/wallet/:id/zap', con(function* (req, res, send, next) { var account = req.options.account; var age = req.options.age; - - req.wallet.zap(account, age, function(err) { - if (err) - return next(err); - - send(200, { success: true }); - }); - }); + yield req.wallet.zap(account, age); + send(200, { success: true }); + })); // Abandon Wallet TX - this.del('/wallet/:id/tx/:hash', function(req, res, next, send) { + this.del('/wallet/:id/tx/:hash', con(function* (req, res, send, next) { var hash = req.options.hash; - req.wallet.abandon(hash, function(err) { - if (err) - return next(err); - - send(200, { success: true }); - }); - }); + yield req.wallet.abandon(hash); + send(200, { success: true }); + })); // Add key - this.put('/wallet/:id/key', function(req, res, next, send) { + this.put('/wallet/:id/key', con(function* (req, res, send, next) { var account = req.options.account; var key = req.options.key; - req.wallet.addKey(account, key, function(err) { - if (err) - return next(err); - - send(200, { success: true }); - }); - }); + yield req.wallet.addKey(account, key); + send(200, { success: true }); + })); // Remove key - this.del('/wallet/:id/key', function(req, res, next, send) { + this.del('/wallet/:id/key', con(function* (req, res, send, next) { var account = req.options.account; var key = req.options.key; - req.wallet.removeKey(account, key, function(err) { - if (err) - return next(err); - - send(200, { success: true }); - }); - }); + yield req.wallet.removeKey(account, key); + send(200, { success: true }); + })); // Create address - this.post('/wallet/:id/address', function(req, res, next, send) { + this.post('/wallet/:id/address', con(function* (req, res, send, next) { var account = req.options.account; - req.wallet.createReceive(account, function(err, address) { - if (err) - return next(err); + var address = yield req.wallet.createReceive(account); + send(200, address.toJSON()); + })); - send(200, address.toJSON()); - }); - }); + // Create nested address + this.post('/wallet/:id/nested', con(function* (req, res, send, next) { + var account = req.options.account; + var address = yield req.wallet.createNested(account); + send(200, address.toJSON()); + })); // Wallet Balance - this.get('/wallet/:id/balance', function(req, res, next, send) { + this.get('/wallet/:id/balance', con(function* (req, res, send, next) { var account = req.options.account; - req.wallet.getBalance(account, function(err, balance) { - if (err) - return next(err); + var balance = yield req.wallet.getBalance(account); - if (!balance) - return send(404); + if (!balance) + return send(404); - send(200, balance.toJSON()); - }); - }); + send(200, balance.toJSON()); + })); // Wallet UTXOs - this.get('/wallet/:id/coin', function(req, res, next, send) { + this.get('/wallet/:id/coin', con(function* (req, res, send, next) { var account = req.options.account; - req.wallet.getCoins(account, function(err, coins) { - if (err) - return next(err); - - send(200, coins.map(function(coin) { - return coin.toJSON(); - })); - }); - }); + var coins = yield req.wallet.getCoins(account); + send(200, coins.map(function(coin) { + return coin.toJSON(); + })); + })); // Wallet Coin - this.get('/wallet/:id/coin/:hash/:index', function(req, res, next, send) { + this.get('/wallet/:id/coin/:hash/:index', con(function* (req, res, send, next) { var hash = req.options.hash; var index = req.options.index; - req.wallet.getCoin(hash, index, function(err, coin) { - if (err) - return next(err); + var coin = yield req.wallet.getCoin(hash, index); - if (!coin) - return send(404); + if (!coin) + return send(404); - send(200, coin.toJSON()); - }); - }); + send(200, coin.toJSON()); + })); // Wallet TXs - this.get('/wallet/:id/tx/history', function(req, res, next, send) { + this.get('/wallet/:id/tx/history', con(function* (req, res, send, next) { var account = req.options.account; - req.wallet.getHistory(account, function(err, txs) { - if (err) - return next(err); - - req.wallet.toDetails(txs, function(err, txs) { - if (err) - return next(err); - - send(200, txs.map(function(tx) { - return tx.toJSON(); - })); - }); - }); - }); + var txs = yield req.wallet.getHistory(account); + var details = yield req.wallet.toDetails(txs); + send(200, details.map(function(tx) { + return tx.toJSON(); + })); + })); // Wallet Pending TXs - this.get('/wallet/:id/tx/unconfirmed', function(req, res, next, send) { + this.get('/wallet/:id/tx/unconfirmed', con(function* (req, res, send, next) { var account = req.options.account; - req.wallet.getUnconfirmed(account, function(err, txs) { - if (err) - return next(err); - - req.wallet.toDetails(txs, function(err, txs) { - if (err) - return next(err); - - send(200, txs.map(function(tx) { - return tx.toJSON(); - })); - }); - }); - }); + var txs = yield req.wallet.getUnconfirmed(account); + var details = yield req.wallet.toDetails(txs); + send(200, details.map(function(tx) { + return tx.toJSON(); + })); + })); // Wallet TXs within time range - this.get('/wallet/:id/tx/range', function(req, res, next, send) { + this.get('/wallet/:id/tx/range', con(function* (req, res, send, next) { var account = req.options.account; var options = req.options; - req.wallet.getRange(account, options, function(err, txs) { - if (err) - return next(err); - - req.wallet.toDetails(txs, function(err, txs) { - if (err) - return next(err); - - send(200, txs.map(function(tx) { - return tx.toJSON(); - })); - }); - }); - }); + var txs = yield req.wallet.getRange(account, options); + var details = yield req.wallet.toDetails(txs); + send(200, details.map(function(tx) { + return tx.toJSON(); + })); + })); // Last Wallet TXs - this.get('/wallet/:id/tx/last', function(req, res, next, send) { + this.get('/wallet/:id/tx/last', con(function* (req, res, send, next) { var account = req.options.account; var limit = req.options.limit; - req.wallet.getLast(account, limit, function(err, txs) { - if (err) - return next(err); - - req.wallet.toDetails(txs, function(err, txs) { - if (err) - return next(err); - - send(200, txs.map(function(tx) { - return tx.toJSON(); - })); - }); - }); - }); + var txs = yield req.wallet.getLast(account, limit); + var details = yield req.wallet.toDetails(txs); + send(200, details.map(function(tx) { + return tx.toJSON(); + })); + })); // Wallet TX - this.get('/wallet/:id/tx/:hash', function(req, res, next, send) { + this.get('/wallet/:id/tx/:hash', con(function* (req, res, send, next) { var hash = req.options.hash; - req.wallet.getTX(hash, function(err, tx) { - if (err) - return next(err); + var tx = yield req.wallet.getTX(hash); + var details; - if (!tx) - return send(404); + if (!tx) + return send(404); - req.wallet.toDetails(tx, function(err, tx) { - if (err) - return next(err); - send(200, tx.toJSON()); - }); - }); - }); + details = yield req.wallet.toDetails(tx); + send(200, details.toJSON()); + })); this.server.on('error', function(err) { self.emit('error', err); @@ -1018,12 +884,7 @@ HTTPServer.prototype._initIO = function _initIO() { if (!utils.isHex256(token)) return callback({ error: 'Invalid parameter.' }); - self.walletdb.auth(id, token, function(err, wallet) { - if (err) { - self.logger.info('Wallet auth failure for %s: %s.', id, err.message); - return callback({ error: 'Bad token.' }); - } - + self.walletdb.auth(id, token).then(function(wallet) { if (!wallet) return callback({ error: 'Wallet does not exist.' }); @@ -1032,6 +893,9 @@ HTTPServer.prototype._initIO = function _initIO() { socket.join(id); callback(); + }, function(err) { + self.logger.info('Wallet auth failure for %s: %s.', id, err.message); + return callback({ error: 'Bad token.' }); }); }); @@ -1089,13 +953,14 @@ HTTPServer.prototype._initIO = function _initIO() { socket.on('scan chain', function(args, callback) { var start = args[0]; - if (!utils.isHex256(start) && !utils.isNumber(start)) + if (!utils.isHex256(start) && !utils.isUInt32(start)) return callback({ error: 'Invalid parameter.' }); - socket.scan(start, function(err) { - if (err) - return callback({ error: err.message }); - callback(); + if (typeof start === 'string') + start = utils.revHex(start); + + socket.scan(start).then(callback, function(err) { + callback({ error: err.message }); }); }); }); @@ -1141,35 +1006,29 @@ HTTPServer.prototype._initIO = function _initIO() { /** * Open the server, wait for socket. - * @param {Function} callback + * @returns {Promise} */ -HTTPServer.prototype.open = function open(callback) { - var self = this; - this.server.open(function(err) { - if (err) - return callback(err); +HTTPServer.prototype.open = co(function* open() { + yield this.server.open(); - self.logger.info('HTTP server loaded.'); + this.logger.info('HTTP server loaded.'); - if (self.apiKey) { - self.logger.info('HTTP API key: %s', self.apiKey); - self.apiKey = null; - } else if (!self.apiHash) { - self.logger.warning('WARNING: Your http server is open to the world.'); - } - - callback(); - }); -}; + if (this.apiKey) { + this.logger.info('HTTP API key: %s', this.apiKey); + this.apiKey = null; + } else if (!this.apiHash) { + this.logger.warning('WARNING: Your http server is open to the world.'); + } +}); /** * Close the server, wait for server socket to close. - * @param {Function} callback + * @returns {Promise} */ -HTTPServer.prototype.close = function close(callback) { - this.server.close(callback); +HTTPServer.prototype.close = function close() { + return this.server.close(); }; /** @@ -1177,7 +1036,7 @@ HTTPServer.prototype.close = function close(callback) { */ HTTPServer.prototype.use = function use(path, callback) { - return this.server.use(path, callback); + return this.server.use(path, callback, this); }; /** @@ -1185,7 +1044,7 @@ HTTPServer.prototype.use = function use(path, callback) { */ HTTPServer.prototype.get = function get(path, callback) { - return this.server.get(path, callback); + return this.server.get(path, callback, this); }; /** @@ -1193,7 +1052,7 @@ HTTPServer.prototype.get = function get(path, callback) { */ HTTPServer.prototype.post = function post(path, callback) { - return this.server.post(path, callback); + return this.server.post(path, callback, this); }; /** @@ -1201,7 +1060,7 @@ HTTPServer.prototype.post = function post(path, callback) { */ HTTPServer.prototype.put = function put(path, callback) { - return this.server.put(path, callback); + return this.server.put(path, callback, this); }; /** @@ -1209,15 +1068,15 @@ HTTPServer.prototype.put = function put(path, callback) { */ HTTPServer.prototype.del = function del(path, callback) { - return this.server.del(path, callback); + return this.server.del(path, callback, this); }; /** * @see HTTPBase#listen */ -HTTPServer.prototype.listen = function listen(port, host, callback) { - this.server.listen(port, host, callback); +HTTPServer.prototype.listen = function listen(port, host) { + return this.server.listen(port, host); }; /** @@ -1287,7 +1146,7 @@ ClientSocket.prototype.addFilter = function addFilter(addresses) { var i, hash; for (i = 0; i < addresses.length; i++) { - hash = bcoin.address.getHash(addresses[i], 'hex'); + hash = Address.getHash(addresses[i], 'hex'); if (!hash) throw new Error('Bad address.'); @@ -1305,7 +1164,7 @@ ClientSocket.prototype.removeFilter = function removeFilter(addresses) { var i, hash; for (i = 0; i < addresses.length; i++) { - hash = bcoin.address.getHash(addresses[i], 'hex'); + hash = Address.getHash(addresses[i], 'hex'); if (!hash) throw new Error('Bad address.'); @@ -1411,27 +1270,40 @@ ClientSocket.prototype.testFilter = function testFilter(tx) { } }; -ClientSocket.prototype.scan = function scan(start, callback) { - var self = this; - var i; +ClientSocket.prototype.scan = co(function* scan(start) { + var scanner = this.scanner.bind(this); + var entry; - if (typeof start === 'string') - start = utils.revHex(start); + if (this.chain.db.options.spv) { + entry = yield this.chain.db.get(start); - if (this.chain.db.options.spv) - return this.chain.reset(start, callback); + if (!entry) + throw new Error('Block not found.'); + + if (!entry.isGenesis()) + start = entry.prevBlock; + + yield this.chain.reset(start); + + return; + } if (this.chain.db.options.prune) - return callback(new Error('Cannot scan in pruned mode.')); + throw new Error('Cannot scan in pruned mode.'); - this.chain.db.scan(start, this.filter, function(entry, txs, next) { - for (i = 0; i < txs.length; i++) - txs[i] = txs[i].toJSON(); + yield this.chain.db.scan(start, this.filter, scanner); +}); - self.emit('block tx', entry.toJSON(), txs); +ClientSocket.prototype.scanner = function scanner(entry, txs) { + var json = new Array(txs.length); + var i; - next(); - }, callback); + for (i = 0; i < txs.length; i++) + json[i] = txs[i].toJSON(); + + this.emit('block tx', entry.toJSON(), json); + + return Promise.resolve(null); }; ClientSocket.prototype.join = function join(id) { diff --git a/lib/http/wallet.js b/lib/http/wallet.js index ad405248..8aad9cfa 100644 --- a/lib/http/wallet.js +++ b/lib/http/wallet.js @@ -11,6 +11,7 @@ var Network = require('../protocol/network'); var EventEmitter = require('events').EventEmitter; var utils = require('../utils/utils'); +var co = require('../utils/co'); var Client = require('./client'); /** @@ -85,11 +86,11 @@ HTTPWallet.prototype._init = function _init() { /** * Open the client and get a wallet. * @alias HTTPWallet#open - * @param {Function} callback + * @returns {Promise} */ -HTTPWallet.prototype.open = function open(options, callback) { - var self = this; +HTTPWallet.prototype.open = co(function* open(options) { + var wallet; this.id = options.id; @@ -100,159 +101,143 @@ HTTPWallet.prototype.open = function open(options, callback) { this.client.token = this.token; } - this.client.open(function(err) { - if (err) - return callback(err); + yield this.client.open(); - self.client.getWallet(self.id, function(err, wallet) { - if (err) - return callback(err); - self.client.join(self.id, wallet.token, function(err) { - if (err) - return callback(new Error(err.error)); - callback(null, wallet); - }); - }); - }); -}; + wallet = yield this.client.getWallet(this.id); + + yield this.client.join(this.id, wallet.token); + + return wallet; +}); /** * Open the client and create a wallet. * @alias HTTPWallet#open - * @param {Function} callback + * @returns {Promise} */ -HTTPWallet.prototype.create = function create(options, callback) { - var self = this; - - this.client.open(function(err) { - if (err) - return callback(err); - - self.client.createWallet(options, function(err, wallet) { - if (err) - return callback(err); - - self.open({ - id: wallet.id, - token: wallet.token - }, callback); - }); +HTTPWallet.prototype.create = co(function* create(options) { + var wallet; + yield this.client.open(); + wallet = yield this.client.createWallet(options); + return yield this.open({ + id: wallet.id, + token: wallet.token }); -}; +}); /** * Close the client, wait for the socket to close. * @alias HTTPWallet#close - * @param {Function} callback + * @returns {Promise} */ -HTTPWallet.prototype.close = function close(callback) { - this.client.close(callback); +HTTPWallet.prototype.close = function close() { + return this.client.close(); }; /** * @see Wallet#getHistory */ -HTTPWallet.prototype.getHistory = function getHistory(account, callback) { - this.client.getHistory(this.id, account, callback); +HTTPWallet.prototype.getHistory = function getHistory(account) { + return this.client.getHistory(this.id, account); }; /** * @see Wallet#getCoins */ -HTTPWallet.prototype.getCoins = function getCoins(account, callback) { - this.client.getCoins(this.id, account, callback); +HTTPWallet.prototype.getCoins = function getCoins(account) { + return this.client.getCoins(this.id, account); }; /** * @see Wallet#getUnconfirmed */ -HTTPWallet.prototype.getUnconfirmed = function getUnconfirmed(account, callback) { - this.client.getUnconfirmed(this.id, account, callback); +HTTPWallet.prototype.getUnconfirmed = function getUnconfirmed(account) { + return this.client.getUnconfirmed(this.id, account); }; /** * @see Wallet#getBalance */ -HTTPWallet.prototype.getBalance = function getBalance(account, callback) { - this.client.getBalance(this.id, account, callback); +HTTPWallet.prototype.getBalance = function getBalance(account) { + return this.client.getBalance(this.id, account); }; /** * @see Wallet#getLast */ -HTTPWallet.prototype.getLast = function getLast(account, limit, callback) { - this.client.getLast(this.id, account, limit, callback); +HTTPWallet.prototype.getLast = function getLast(account, limit) { + return this.client.getLast(this.id, account, limit); }; /** * @see Wallet#getRange */ -HTTPWallet.prototype.getRange = function getRange(account, options, callback) { - this.client.getRange(this.id, account, options, callback); +HTTPWallet.prototype.getRange = function getRange(account, options) { + return this.client.getRange(this.id, account, options); }; /** * @see Wallet#getTX */ -HTTPWallet.prototype.getTX = function getTX(account, hash, callback) { - this.client.getWalletTX(this.id, account, hash, callback); +HTTPWallet.prototype.getTX = function getTX(account, hash) { + return this.client.getWalletTX(this.id, account, hash); }; /** * @see Wallet#getCoin */ -HTTPWallet.prototype.getCoin = function getCoin(account, hash, index, callback) { - this.client.getWalletCoin(this.id, account, hash, index, callback); +HTTPWallet.prototype.getCoin = function getCoin(account, hash, index) { + return this.client.getWalletCoin(this.id, account, hash, index); }; /** * @see Wallet#zap */ -HTTPWallet.prototype.zap = function zap(account, age, callback) { - this.client.zap(this.id, account, age, callback); +HTTPWallet.prototype.zap = function zap(account, age) { + return this.client.zap(this.id, account, age); }; /** * @see Wallet#createTX */ -HTTPWallet.prototype.createTX = function createTX(options, outputs, callback) { - this.client.createTX(this.id, options, outputs, callback); +HTTPWallet.prototype.createTX = function createTX(options, outputs) { + return this.client.createTX(this.id, options, outputs); }; /** * @see HTTPClient#walletSend */ -HTTPWallet.prototype.send = function send(options, callback) { - this.client.send(this.id, options, callback); +HTTPWallet.prototype.send = function send(options) { + return this.client.send(this.id, options); }; /** * @see Wallet#sign */ -HTTPWallet.prototype.sign = function sign(tx, options, callback) { - this.client.sign(this.id, tx, options, callback); +HTTPWallet.prototype.sign = function sign(tx, options) { + return this.client.sign(this.id, tx, options); }; /** * @see Wallet#fillCoins */ -HTTPWallet.prototype.fillCoins = function fillCoins(tx, callback) { - this.client.fillCoins(tx, callback); +HTTPWallet.prototype.fillCoins = function fillCoins(tx) { + return this.client.fillCoins(tx); }; /** @@ -260,7 +245,7 @@ HTTPWallet.prototype.fillCoins = function fillCoins(tx, callback) { */ HTTPWallet.prototype.getInfo = function getInfo(callback) { - this.client.getWallet(this.id, callback); + return this.client.getWallet(this.id); }; /** @@ -268,63 +253,61 @@ HTTPWallet.prototype.getInfo = function getInfo(callback) { */ HTTPWallet.prototype.getAccounts = function getAccounts(callback) { - this.client.getAccounts(this.id, callback); + return this.client.getAccounts(this.id); }; /** * @see Wallet#getAccount */ -HTTPWallet.prototype.getAccount = function getAccount(account, callback) { - this.client.getAccount(this.id, account, callback); +HTTPWallet.prototype.getAccount = function getAccount(account) { + return this.client.getAccount(this.id, account); }; /** * @see Wallet#createAccount */ -HTTPWallet.prototype.createAccount = function createAccount(options, callback) { - this.client.createAccount(this.id, options, callback); +HTTPWallet.prototype.createAccount = function createAccount(options) { + return this.client.createAccount(this.id, options); }; /** * @see Wallet#createAddress */ -HTTPWallet.prototype.createAddress = function createAddress(account, callback) { - this.client.createAddress(this.id, account, callback); +HTTPWallet.prototype.createAddress = function createAddress(account) { + return this.client.createAddress(this.id, account); +}; + +/** + * @see Wallet#createAddress + */ + +HTTPWallet.prototype.createNested = function createNested(account) { + return this.client.createNested(this.id, account); }; /** * @see Wallet#setPassphrase */ -HTTPWallet.prototype.setPassphrase = function setPassphrase(old, new_, callback) { - this.client.setPassphrase(this.id, old, new_, callback); +HTTPWallet.prototype.setPassphrase = function setPassphrase(old, new_) { + return this.client.setPassphrase(this.id, old, new_); }; /** * @see Wallet#retoken */ -HTTPWallet.prototype.retoken = function retoken(passphrase, callback) { - var self = this; +HTTPWallet.prototype.retoken = co(function* retoken(passphrase) { + var token = yield this.client.retoken(this.id, passphrase); - if (typeof passphrase === 'function') { - callback = passphrase; - passphrase = null; - } + this.token = token; + this.client.token = token; - this.client.retoken(this.id, passphrase, function(err, token) { - if (err) - return callback(err); - - self.token = token; - self.client.token = token; - - return callback(null, token); - }); -}; + return token; +}); /* * Expose diff --git a/lib/mempool/fees.js b/lib/mempool/fees.js index d5d2f852..bc3436ab 100644 --- a/lib/mempool/fees.js +++ b/lib/mempool/fees.js @@ -8,13 +8,14 @@ 'use strict'; -var bcoin = require('../env'); -var utils = bcoin.utils; +var utils = require('../utils/utils'); var assert = require('assert'); -var constants = bcoin.constants; +var constants = require('../protocol/constants'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); -var global = bcoin.utils.global; +var Logger = require('../node/logger'); +var Network = require('../protocol/network'); +var global = utils.global; var Float64Array = global.Float64Array || Array; var Int32Array = global.Int32Array || Array; @@ -54,7 +55,7 @@ function ConfirmStats(buckets, maxConfirms, decay, type, logger) { if (!(this instanceof ConfirmStats)) return new ConfirmStats(buckets, maxConfirms, decay, type, logger); - this.logger = logger || bcoin.defaultLogger; + this.logger = logger || Logger.global; this.maxConfirms = maxConfirms; this.decay = decay; this.type = type; @@ -399,8 +400,8 @@ function PolicyEstimator(minRelay, network, logger) { fee = []; priority = []; - this.network = bcoin.network.get(network); - this.logger = logger || bcoin.defaultLogger; + this.network = Network.get(network); + this.logger = logger || Logger.global; this.minTrackedFee = minRelay < MIN_FEERATE ? MIN_FEERATE @@ -776,7 +777,7 @@ PolicyEstimator.prototype.toRaw = function toRaw(writer) { PolicyEstimator.fromRaw = function fromRaw(minRelay, data, logger) { var p = new BufferReader(data); - var network = bcoin.network.fromMagic(p.readU32()); + var network = Network.fromMagic(p.readU32()); var bestHeight = p.readU32(); var estimator = new PolicyEstimator(minRelay, network, logger); var feeStats = ConfirmStats.fromRaw(p.readVarBytes(), 'FeeRate', logger); diff --git a/lib/mempool/index.js b/lib/mempool/index.js new file mode 100644 index 00000000..e7f1d57c --- /dev/null +++ b/lib/mempool/index.js @@ -0,0 +1,5 @@ +'use strict'; + +exports.Mempool = require('./mempool'); +exports.MempoolEntry = require('./mempoolentry'); +exports.Fees = require('./fees'); diff --git a/lib/mempool/mempool.js b/lib/mempool/mempool.js index 2bb92e1f..0a1c4f8f 100644 --- a/lib/mempool/mempool.js +++ b/lib/mempool/mempool.js @@ -1,23 +1,27 @@ /*! * mempool.js - mempool for bcoin - * Copyright (c) 2014-2015, Fedor Indutny (MIT License) * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; -var bcoin = require('../env'); var AsyncObject = require('../utils/async'); -var constants = bcoin.constants; +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); -var assert = utils.assert; -var BufferWriter = require('../utils/writer'); -var BufferReader = require('../utils/reader'); +var co = require('../utils/co'); +var assert = require('assert'); var crypto = require('../crypto/crypto'); -var VerifyError = bcoin.errors.VerifyError; +var VerifyError = require('../utils/errors').VerifyError; var VerifyResult = utils.VerifyResult; var flags = constants.flags; +var Bloom = require('../utils/bloom'); +var Address = require('../primitives/address'); +var Coin = require('../primitives/coin'); +var Locker = require('../utils/locker'); +var Outpoint = require('../primitives/outpoint'); +var TX = require('../primitives/tx'); +var MempoolEntry = require('./mempoolentry'); /** * Represents a mempool. @@ -44,8 +48,8 @@ var flags = constants.flags; * @property {Number} maxSize * @property {Boolean} blockSinceBump * @property {Number} lastFeeUpdate - * @property {Rate} minFeeRate - * @property {Rate} minReasonableFee + * @property {Rate} minRate + * @property {Rate} minReasonable * @property {Rate} minRelayFee * @emits Mempool#open * @emits Mempool#error @@ -73,7 +77,7 @@ function Mempool(options) { this.logger = options.logger || this.chain.logger; this.loaded = false; - this.locker = new bcoin.locker(this, this.addTX); + this.locker = new Locker(true); this.size = 0; this.totalOrphans = 0; @@ -83,10 +87,11 @@ function Mempool(options) { this.orphans = {}; this.tx = {}; this.spents = {}; + this.currentTX = null; this.coinIndex = new AddressIndex(this); this.txIndex = new AddressIndex(this); - this.rejects = new bcoin.bloom.rolling(120000, 0.000001); + this.rejects = new Bloom.Rolling(120000, 0.000001); this.freeCount = 0; this.lastTime = 0; @@ -94,6 +99,7 @@ function Mempool(options) { this.limitFree = this.options.limitFree !== false; this.limitFreeRelay = this.options.limitFreeRelay || 15; this.relayPriority = this.options.relayPriority !== false; + this.rejectFee = this.options.rejectFee === true; this.requireStandard = this.options.requireStandard != null ? this.options.requireStandard : this.network.requireStandard; @@ -105,9 +111,9 @@ function Mempool(options) { this.expiryTime = options.expiryTime || constants.mempool.MEMPOOL_EXPIRY; this.blockSinceBump = false; this.lastFeeUpdate = utils.now(); - this.minFeeRate = 0; - this.minReasonableFee = constants.tx.MIN_RELAY; - this.minRelayFee = constants.tx.MIN_RELAY; + this.minRate = 0; + this.minReasonable = this.network.minRelay; + this.minRelay = this.network.minRelay; } utils.inherits(Mempool, AsyncObject); @@ -115,40 +121,23 @@ utils.inherits(Mempool, AsyncObject); /** * Open the chain, wait for the database to load. * @alias Mempool#open - * @param {Function} callback + * @returns {Promise} */ -Mempool.prototype._open = function open(callback) { - var self = this; +Mempool.prototype._open = co(function* open() { var size = (this.maxSize / 1024).toFixed(2); - this.chain.open(function(err) { - if (err) - return callback(err); - - self.logger.info('Mempool loaded (maxsize=%dkb).', size); - - callback(); - }); -}; + yield this.chain.open(); + this.logger.info('Mempool loaded (maxsize=%dkb).', size); +}); /** * Close the chain, wait for the database to close. * @alias Mempool#close - * @param {Function} callback + * @returns {Promise} */ -Mempool.prototype._close = function close(callback) { - callback(); -}; - -/** - * Invoke mutex lock. - * @private - * @returns {Function} unlock - */ - -Mempool.prototype._lock = function _lock(func, args, force) { - return this.locker.lock(func, args, force); +Mempool.prototype._close = function close() { + return Promise.resolve(null); }; /** @@ -156,18 +145,30 @@ Mempool.prototype._lock = function _lock(func, args, force) { * in (removes all transactions contained in the * block from the mempool). * @param {Block} block - * @param {Function} callback + * @returns {Promise} */ -Mempool.prototype.addBlock = function addBlock(block, callback) { +Mempool.prototype.addBlock = co(function* addBlock(block) { + var unlock = yield this.locker.lock(); + try { + return yield this._addBlock(block); + } finally { + unlock(); + } +}); + +/** + * Notify the mempool that a new block + * has come without a lock. + * @private + * @param {Block} block + * @returns {Promise} + */ + +Mempool.prototype._addBlock = co(function* addBlock(block) { var entries = []; var i, entry, tx, hash; - callback = this._lock(addBlock, [block, callback]); - - if (!callback) - return; - for (i = block.txs.length - 1; i >= 0; i--) { tx = block.txs[i]; hash = tx.hash('hex'); @@ -198,53 +199,60 @@ Mempool.prototype.addBlock = function addBlock(block, callback) { // There may be a locktime in a TX that is now valid. this.rejects.reset(); - utils.nextTick(callback); -}; + yield co.wait(); +}); /** * Notify the mempool that a block has been disconnected * from the main chain (reinserts transactions into the mempool). * @param {Block} block - * @param {Function} callback + * @returns {Promise} */ -Mempool.prototype.removeBlock = function removeBlock(block, callback) { - var self = this; - var entry; +Mempool.prototype.removeBlock = co(function* removeBlock(block) { + var unlock = yield this.lock(); + try { + return yield this._removeBlock(block); + } finally { + unlock(); + } +}); - callback = this._lock(removeBlock, [block, callback]); +/** + * Notify the mempool that a block + * has been disconnected without a lock. + * @private + * @param {Block} block + * @returns {Promise} + */ - if (!callback) - return; +Mempool.prototype._removeBlock = co(function* removeBlock(block) { + var i, entry, tx, hash; - this.rejects.reset(); - - utils.forEachSerial(block.txs, function(tx, next) { - var hash = tx.hash('hex'); + for (i = 0; i < block.txs.length; i++) { + tx = block.txs[i]; + hash = tx.hash('hex'); if (tx.isCoinbase()) - return next(); + continue; - if (self.hasTX(hash)) - return next(); + if (this.hasTX(hash)) + continue; entry = MempoolEntry.fromTX(tx, block.height); - self.addUnchecked(entry, function(err) { - if (err) - return next(err); + yield this._addUnchecked(entry); - self.emit('unconfirmed', tx, block); + this.emit('unconfirmed', tx, block); + } - next(); - }, true); - }, callback); -}; + this.rejects.reset(); +}); /** * Ensure the size of the mempool stays below 300mb. * @param {Hash} entryHash - TX that initiated the trim. - * @param {Function} callback + * @returns {Promise} */ Mempool.prototype.limitMempoolSize = function limitMempoolSize(entryHash) { @@ -360,7 +368,7 @@ Mempool.prototype.getCoin = function getCoin(hash, index) { if (index >= entry.tx.outputs.length) return; - return bcoin.coin.fromTX(entry.tx, index); + return Coin.fromTX(entry.tx, index); }; /** @@ -391,7 +399,8 @@ Mempool.prototype.getCoinsByAddress = function getCoinsByAddress(addresses) { addresses = [addresses]; for (i = 0; i < addresses.length; i++) { - hash = bcoin.address.getHash(addresses[i], 'hex'); + hash = Address.getHash(addresses[i], 'hex'); + if (!hash) continue; @@ -418,7 +427,8 @@ Mempool.prototype.getTXByAddress = function getTXByAddress(addresses) { addresses = [addresses]; for (i = 0; i < addresses.length; i++) { - hash = bcoin.address.getHash(addresses[i], 'hex'); + hash = Address.getHash(addresses[i], 'hex'); + if (!hash) continue; @@ -457,7 +467,7 @@ Mempool.prototype.fillHistory = function fillHistory(tx) { if (!prev) continue; - input.coin = bcoin.coin.fromTX(prev, prevout.index); + input.coin = Coin.fromTX(prev, prevout.index); } }; @@ -510,6 +520,21 @@ Mempool.prototype.has = function has(hash) { if (this.locker.hasPending(hash)) return true; + if (hash === this.currentTX) + return true; + + return this.exists(hash); +}; + +/** + * Test the mempool to see if it + * contains a transaction or an orphan. + * @private + * @param {Hash} hash + * @returns {Boolean} + */ + +Mempool.prototype.exists = function exists(hash) { if (this.hasOrphan(hash)) return true; @@ -532,166 +557,143 @@ Mempool.prototype.hasReject = function hasReject(hash) { * will lock the mempool until the transaction is * fully processed. * @param {TX} tx - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Mempool.prototype.addTX = function addTX(tx, callback) { - var self = this; - this._addTX(tx, function(err, missing) { - if (err) { - if (err.type === 'VerifyError') { - if (!tx.hasWitness() && !err.malleated) - self.rejects.add(tx.hash()); +Mempool.prototype.addTX = co(function* addTX(tx) { + var unlock = yield this.locker.lock(tx); - return callback(err); - } - return callback(err); + assert(!this.currentTX); + + this.currentTX = tx.hash('hex'); + + try { + return yield this._addTX(tx); + } catch (err) { + if (err.type === 'VerifyError') { + if (!tx.hasWitness() && !err.malleated) + this.rejects.add(tx.hash()); } - callback(null, missing); - }); -}; + throw err; + } finally { + this.currentTX = null; + unlock(); + } +}); /** - * Add a transaction to the mempool. + * Add a transaction to the mempool without a lock. * @private * @param {TX} tx - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Mempool.prototype._addTX = function _addTX(tx, callback) { - var self = this; +Mempool.prototype._addTX = co(function* _addTX(tx) { var lockFlags = constants.flags.STANDARD_LOCKTIME_FLAGS; var hash = tx.hash('hex'); - var ret, entry, missing; - - callback = this._lock(_addTX, [tx, callback]); - - if (!callback) - return; - - callback = utils.asyncify(callback); + var ret, entry, result, exists; assert(!tx.mutable, 'Cannot add mutable TX to mempool.'); ret = new VerifyResult(); if (tx.ts !== 0) { - return callback(new VerifyError(tx, + throw new VerifyError(tx, 'alreadyknown', 'txn-already-known', - 0)); + 0); } if (!tx.isSane(ret)) { - return callback(new VerifyError(tx, + throw new VerifyError(tx, 'invalid', ret.reason, - ret.score)); + ret.score); } if (tx.isCoinbase()) { - return callback(new VerifyError(tx, + throw new VerifyError(tx, 'invalid', 'coinbase', - 100)); + 100); } if (this.requireStandard) { if (!this.chain.state.hasCSV() && tx.version >= 2) { - return callback(new VerifyError(tx, + throw new VerifyError(tx, 'nonstandard', 'premature-version2-tx', - 0)); + 0); } } if (!this.chain.state.hasWitness() && !this.prematureWitness) { if (tx.hasWitness()) { - return callback(new VerifyError(tx, + throw new VerifyError(tx, 'nonstandard', 'no-witness-yet', - 0)); + 0); } } if (this.requireStandard) { if (!tx.isStandard(ret)) { - return callback(new VerifyError(tx, + throw new VerifyError(tx, 'nonstandard', ret.reason, - ret.score)); + ret.score); } } - this.chain.checkFinal(this.chain.tip, tx, lockFlags, function(err, result) { - if (err) - return callback(err); + result = yield this.chain.checkFinal(this.chain.tip, tx, lockFlags); - if (!result) { - return callback(new VerifyError(tx, - 'nonstandard', - 'non-final', - 0)); - } + if (!result) { + throw new VerifyError(tx, + 'nonstandard', + 'non-final', + 0); + } - if (self.has(hash)) { - return callback(new VerifyError(tx, - 'alreadyknown', - 'txn-already-in-mempool', - 0)); - } + if (this.exists(hash)) { + throw new VerifyError(tx, + 'alreadyknown', + 'txn-already-in-mempool', + 0); + } - self.chain.db.hasCoins(hash, function(err, exists) { - if (err) - return callback(err); + exists = yield this.chain.db.hasCoins(hash); - if (exists) { - return callback(new VerifyError(tx, - 'alreadyknown', - 'txn-already-known', - 0)); - } + if (exists) { + throw new VerifyError(tx, + 'alreadyknown', + 'txn-already-known', + 0); + } - if (self.isDoubleSpend(tx)) { - return callback(new VerifyError(tx, - 'duplicate', - 'bad-txns-inputs-spent', - 0)); - } + if (this.isDoubleSpend(tx)) { + throw new VerifyError(tx, + 'duplicate', + 'bad-txns-inputs-spent', + 0); + } - self.fillAllCoins(tx, function(err) { - if (err) - return callback(err); + yield this.fillAllCoins(tx); - if (!tx.hasCoins()) { - missing = self.storeOrphan(tx); - return callback(null, missing); - } + if (!tx.hasCoins()) + return this.storeOrphan(tx); - entry = MempoolEntry.fromTX(tx, self.chain.height); + entry = MempoolEntry.fromTX(tx, this.chain.height); - self.verify(entry, function(err) { - if (err) - return callback(err); + yield this.verify(entry); + yield this._addUnchecked(entry); - self.addUnchecked(entry, function(err) { - if (err) - return callback(err); - - if (self.limitMempoolSize(hash)) { - return callback(new VerifyError(tx, - 'insufficientfee', - 'mempool full', - 0)); - } - - callback(); - }, true); - }); - }); - }); - }); -}; + if (this.limitMempoolSize(hash)) { + throw new VerifyError(tx, + 'insufficientfee', + 'mempool full', + 0); + } +}); /** * Add a transaction to the mempool without performing any @@ -700,17 +702,27 @@ Mempool.prototype._addTX = function _addTX(tx, callback) { * This function will also resolve orphans if possible (the * resolved orphans _will_ be validated). * @param {MempoolEntry} entry - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Mempool.prototype.addUnchecked = function addUnchecked(entry, callback, force) { - var self = this; - var resolved; +Mempool.prototype.addUnchecked = co(function* addUnchecked(entry) { + var unlock = yield this.locker.lock(); + try { + return yield this._addUnchecked(entry); + } finally { + unlock(); + } +}); - callback = this._lock(addUnchecked, [entry, callback], force); +/** + * Add a transaction to the mempool without a lock. + * @private + * @param {MempoolEntry} entry + * @returns {Promise} + */ - if (!callback) - return; +Mempool.prototype._addUnchecked = co(function* addUnchecked(entry) { + var i, resolved, tx, orphan; this.trackEntry(entry); @@ -724,34 +736,37 @@ Mempool.prototype.addUnchecked = function addUnchecked(entry, callback, force) { resolved = this.resolveOrphans(entry.tx); - utils.forEachSerial(resolved, function(tx, next) { - var entry = MempoolEntry.fromTX(tx, self.chain.height); - self.verify(entry, function(err) { - if (err) { - if (err.type === 'VerifyError') { - self.logger.debug('Could not resolve orphan %s: %s.', - tx.rhash, - err.message); + for (i = 0; i < resolved.length; i++) { + tx = resolved[i]; + orphan = MempoolEntry.fromTX(tx, this.chain.height); - if (!tx.hasWitness() && !err.malleated) - self.rejects.add(tx.hash()); + try { + yield this.verify(orphan); + } catch (err) { + if (err.type === 'VerifyError') { + this.logger.debug('Could not resolve orphan %s: %s.', + tx.rhash, + err.message); - return next(); - } - self.emit('error', err); - return next(); + if (!tx.hasWitness() && !err.malleated) + this.rejects.add(tx.hash()); + + continue; } - self.addUnchecked(entry, function(err) { - if (err) { - self.emit('error', err); - return next(); - } - self.logger.spam('Resolved orphan %s in mempool.', entry.tx.rhash); - next(); - }, true); - }); - }, callback); -}; + this.emit('error', err); + continue; + } + + try { + yield this._addUnchecked(orphan); + } catch (err) { + this.emit('error', err); + continue; + } + + this.logger.spam('Resolved orphan %s in mempool.', orphan.tx.rhash); + } +}); /** * Remove a transaction from the mempool. Generally @@ -783,10 +798,10 @@ Mempool.prototype.removeUnchecked = function removeUnchecked(entry, limit) { if (limit) { this.logger.spam('Removed tx %s from mempool.', entry.tx.rhash); - rate = bcoin.tx.getRate(entry.sizes, entry.fees); - rate += this.minReasonableFee; - if (rate > this.minFeeRate) { - this.minFeeRate = rate; + rate = TX.getRate(entry.sizes, entry.fees); + rate += this.minReasonable; + if (rate > this.minRate) { + this.minRate = rate; this.blockSinceBump = false; } } else { @@ -804,8 +819,8 @@ Mempool.prototype.removeUnchecked = function removeUnchecked(entry, limit) { Mempool.prototype.getMinRate = function getMinRate() { var now, halflife, size; - if (!this.blockSinceBump || this.minFeeRate === 0) - return this.minFeeRate; + if (!this.blockSinceBump || this.minRate === 0) + return this.minRate; now = utils.now(); @@ -818,27 +833,26 @@ Mempool.prototype.getMinRate = function getMinRate() { else if (size < this.maxSize / 2) halflife >>>= 1; - this.minFeeRate /= Math.pow(2.0, (now - this.lastFeeUpdate) / halflife | 0); - this.minFeeRate |= 0; + this.minRate /= Math.pow(2.0, (now - this.lastFeeUpdate) / halflife | 0); + this.minRate |= 0; this.lastFeeUpdate = now; - if (this.minFeeRate < this.minReasonableFee / 2) { - this.minFeeRate = 0; + if (this.minRate < this.minReasonable / 2) { + this.minRate = 0; return 0; } } - return Math.max(this.minFeeRate, this.minReasonableFee); + return Math.max(this.minRate, this.minReasonable); }; /** * Verify a transaction with mempool standards. * @param {TX} tx - * @param {Function} callback - Returns [{@link VerifyError}]. + * @returns {Promise} */ -Mempool.prototype.verify = function verify(entry, callback) { - var self = this; +Mempool.prototype.verify = co(function* verify(entry) { var height = this.chain.height + 1; var lockFlags = flags.STANDARD_LOCKTIME_FLAGS; var flags1 = flags.STANDARD_VERIFY_FLAGS; @@ -847,225 +861,199 @@ Mempool.prototype.verify = function verify(entry, callback) { var mandatory = flags.MANDATORY_VERIFY_FLAGS; var tx = entry.tx; var ret = new VerifyResult(); - var fee, modFee, now, size, rejectFee, minRelayFee, minRate, count; + var fee, modFee, now, size, minRate; + var rejectFee, minRelayFee, count, result; - this.checkLocks(tx, lockFlags, function(err, result) { - if (err) - return callback(err); + result = yield this.checkLocks(tx, lockFlags); - if (!result) { - return callback(new VerifyError(tx, + if (!result) { + throw new VerifyError(tx, + 'nonstandard', + 'non-BIP68-final', + 0); + } + + if (this.requireStandard) { + if (!tx.hasStandardInputs()) { + throw new VerifyError(tx, 'nonstandard', - 'non-BIP68-final', - 0)); + 'bad-txns-nonstandard-inputs', + 0); } - - if (self.requireStandard) { - if (!tx.hasStandardInputs()) { - return callback(new VerifyError(tx, + if (this.chain.state.hasWitness()) { + if (!tx.hasStandardWitness(ret)) { + ret = new VerifyError(tx, 'nonstandard', - 'bad-txns-nonstandard-inputs', - 0)); - } - if (self.chain.state.hasWitness()) { - if (!tx.hasStandardWitness(ret)) { - ret = new VerifyError(tx, - 'nonstandard', - ret.reason, - ret.score); - ret.malleated = ret.score > 0; - return callback(ret); - } + ret.reason, + ret.score); + ret.malleated = ret.score > 0; + throw ret; } } + } - if (tx.getSigopsWeight(flags) > constants.tx.MAX_SIGOPS_WEIGHT) { - return callback(new VerifyError(tx, - 'nonstandard', - 'bad-txns-too-many-sigops', - 0)); - } + if (tx.getSigopsWeight(flags) > constants.tx.MAX_SIGOPS_WEIGHT) { + throw new VerifyError(tx, + 'nonstandard', + 'bad-txns-too-many-sigops', + 0); + } - fee = tx.getFee(); - modFee = entry.fees; - size = entry.size; - minRate = self.getMinRate(); - - if (minRate > self.minRelayFee) - self.network.updateMinRelay(minRate); + fee = tx.getFee(); + modFee = entry.fees; + size = entry.size; + if (this.rejectFee) { + minRate = this.getMinRate(); rejectFee = tx.getMinFee(size, minRate); - minRelayFee = tx.getMinFee(size, self.minRelayFee); - if (rejectFee > 0 && modFee < rejectFee) { - return callback(new VerifyError(tx, + throw new VerifyError(tx, 'insufficientfee', 'mempool min fee not met', - 0)); + 0); + } + } + + minRelayFee = tx.getMinFee(size, this.minRelay); + + if (this.relayPriority && modFee < minRelayFee) { + if (!entry.isFree(height)) { + throw new VerifyError(tx, + 'insufficientfee', + 'insufficient priority', + 0); + } + } + + // Continuously rate-limit free (really, very-low-fee) + // transactions. This mitigates 'penny-flooding'. i.e. + // sending thousands of free transactions just to be + // annoying or make others' transactions take longer + // to confirm. + if (this.limitFree && modFee < minRelayFee) { + now = utils.now(); + + // Use an exponentially decaying ~10-minute window: + this.freeCount *= Math.pow(1 - 1 / 600, now - this.lastTime); + this.lastTime = now; + + // The limitFreeRelay unit is thousand-bytes-per-minute + // At default rate it would take over a month to fill 1GB + if (this.freeCount > this.limitFreeRelay * 10 * 1000) { + throw new VerifyError(tx, + 'insufficientfee', + 'rate limited free transaction', + 0); } - if (self.relayPriority && modFee < minRelayFee) { - if (!entry.isFree(height)) { - return callback(new VerifyError(tx, - 'insufficientfee', - 'insufficient priority', - 0)); - } - } + this.freeCount += size; + } - // Continuously rate-limit free (really, very-low-fee) - // transactions. This mitigates 'penny-flooding'. i.e. - // sending thousands of free transactions just to be - // annoying or make others' transactions take longer - // to confirm. - if (self.limitFree && modFee < minRelayFee) { - now = utils.now(); + if (this.rejectAbsurdFees && fee > minRelayFee * 10000) + throw new VerifyError(tx, 'highfee', 'absurdly-high-fee', 0); - // Use an exponentially decaying ~10-minute window: - self.freeCount *= Math.pow(1 - 1 / 600, now - self.lastTime); - self.lastTime = now; + count = this.countAncestors(tx); - // The limitFreeRelay unit is thousand-bytes-per-minute - // At default rate it would take over a month to fill 1GB - if (self.freeCount > self.limitFreeRelay * 10 * 1000) { - return callback(new VerifyError(tx, - 'insufficientfee', - 'rate limited free transaction', - 0)); - } + if (count > constants.mempool.ANCESTOR_LIMIT) { + throw new VerifyError(tx, + 'nonstandard', + 'too-long-mempool-chain', + 0); + } - self.freeCount += size; - } + if (!tx.checkInputs(height, ret)) + throw new VerifyError(tx, 'invalid', ret.reason, ret.score); - if (self.rejectAbsurdFees && fee > minRelayFee * 10000) - return callback(new VerifyError(tx, 'highfee', 'absurdly-high-fee', 0)); + // Standard verification + try { + yield this.checkInputs(tx, flags1); + } catch (error) { + if (tx.hasWitness()) + throw error; - count = self.countAncestors(tx); + // Try without segwit and cleanstack. + result = yield this.checkResult(tx, flags2); - if (count > constants.mempool.ANCESTOR_LIMIT) { - return callback(new VerifyError(tx, - 'nonstandard', - 'too-long-mempool-chain', - 0)); - } + // If it failed, the first verification + // was the only result we needed. + if (!result) + throw error; - if (!tx.checkInputs(height, ret)) - return callback(new VerifyError(tx, 'invalid', ret.reason, ret.score)); + // If it succeeded, segwit may be causing the + // failure. Try with segwit but without cleanstack. + result = yield this.checkResult(tx, flags3); - // Standard verification - self.checkInputs(tx, flags1, function(error) { - if (!error) { - if (!self.paranoid) - return callback(); + // Cleanstack was causing the failure. + if (result) + throw error; - return self.checkResult(tx, mandatory, function(err, result) { - if (err) { - assert(err.type !== 'VerifyError', - 'BUG: Verification failed for mandatory but not standard.'); - return callback(err); - } - callback(); - }); - } + // Do not insert into reject cache. + error.malleated = true; + throw error; + } - if (error.type !== 'VerifyError') - return callback(error); - - if (tx.hasWitness()) - return callback(error); - - // Try without segwit and cleanstack. - self.checkResult(tx, flags2, function(err, result) { - if (err) - return callback(err); - - // If it failed, the first verification - // was the only result we needed. - if (!result) - return callback(error); - - // If it succeeded, segwit may be causing the - // failure. Try with segwit but without cleanstack. - self.checkResult(tx, flags3, function(err, result) { - if (err) - return callback(err); - - // Cleanstack was causing the failure. - if (result) - return callback(error); - - // Do not insert into reject cache. - error.malleated = true; - callback(error); - }); - }); - }); - }); -}; + // Paranoid checks. + if (this.paranoid) { + result = yield this.checkResult(tx, mandatory); + assert(result, 'BUG: Verify failed for mandatory but not standard.'); + } +}); /** * Verify inputs, return a boolean * instead of an error based on success. * @param {TX} tx * @param {VerifyFlags} flags - * @param {Function} callback + * @returns {Promise} */ -Mempool.prototype.checkResult = function checkResult(tx, flags, callback) { - this.checkInputs(tx, flags, function(err) { - if (err) { - if (err.type === 'VerifyError') - return callback(null, false); - return callback(err); - } - callback(null, true); - }); -}; +Mempool.prototype.checkResult = co(function* checkResult(tx, flags) { + try { + yield this.checkInputs(tx, flags); + } catch (err) { + if (err.type === 'VerifyError') + return false; + throw err; + } + return true; +}); /** * Verify inputs for standard * _and_ mandatory flags on failure. * @param {TX} tx * @param {VerifyFlags} flags - * @param {Function} callback + * @returns {Promise} */ -Mempool.prototype.checkInputs = function checkInputs(tx, flags, callback) { - // Do this in the worker pool. - tx.verifyAsync(flags, function(err, result) { - if (err) - return callback(err); +Mempool.prototype.checkInputs = co(function* checkInputs(tx, flags) { + var result = yield tx.verifyAsync(flags); + if (result) + return; - if (result) - return callback(); + if (!(flags & constants.flags.UNSTANDARD_VERIFY_FLAGS)) { + throw new VerifyError(tx, + 'nonstandard', + 'non-mandatory-script-verify-flag', + 0); + } - if (!(flags & constants.flags.UNSTANDARD_VERIFY_FLAGS)) { - return callback(new VerifyError(tx, - 'nonstandard', - 'non-mandatory-script-verify-flag', - 0)); - } + flags &= ~constants.flags.UNSTANDARD_VERIFY_FLAGS; - flags &= ~constants.flags.UNSTANDARD_VERIFY_FLAGS; + result = yield tx.verifyAsync(flags); - tx.verifyAsync(flags, function(err, result) { - if (err) - return callback(err); + if (result) { + throw new VerifyError(tx, + 'nonstandard', + 'non-mandatory-script-verify-flag', + 0); + } - if (result) { - return callback(new VerifyError(tx, - 'nonstandard', - 'non-mandatory-script-verify-flag', - 0)); - } - - return callback(new VerifyError(tx, - 'nonstandard', - 'mandatory-script-verify-flag', - 100)); - }); - }); -}; + throw new VerifyError(tx, + 'nonstandard', + 'mandatory-script-verify-flag', + 100); +}); /** * Count the highest number of @@ -1081,10 +1069,13 @@ Mempool.prototype.countAncestors = function countAncestors(tx) { for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; prev = this.getTX(input.prevout.hash); + if (!prev) continue; + count = 1; count += this.countAncestors(prev); + if (count > max) max = count; } @@ -1106,10 +1097,13 @@ Mempool.prototype.countDescendants = function countDescendants(tx) { for (i = 0; i < tx.outputs.length; i++) { next = this.isSpent(hash, i); + if (!next) continue; + count = 1; count += this.countDescendants(next.tx); + if (count > max) max = count; } @@ -1133,8 +1127,10 @@ Mempool.prototype.getAncestors = function getAncestors(tx) { for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; prev = self.getTX(input.prevout.hash); + if (!prev) continue; + entries.push(prev); traverse(prev); } @@ -1159,8 +1155,10 @@ Mempool.prototype.getDescendants = function getDescendants(tx) { for (i = 0; i < tx.outputs.length; i++) { next = self.isSpent(hash, i); + if (!next) continue; + entries.push(next); traverse(next.tx); } @@ -1223,8 +1221,10 @@ Mempool.prototype.storeOrphan = function storeOrphan(tx) { for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; + if (input.coin) continue; + missing[input.prevout.hash] = true; } @@ -1234,8 +1234,10 @@ Mempool.prototype.storeOrphan = function storeOrphan(tx) { for (i = 0; i < missing.length; i++) { prev = missing[i]; + if (!this.waiting[prev]) this.waiting[prev] = []; + this.waiting[prev].push(hash); } @@ -1265,9 +1267,12 @@ Mempool.prototype.getBalance = function getBalance() { for (i = 0; i < hashes.length; i++) { hash = hashes[i]; tx = this.getTX(hash); + if (!tx) continue; + hash = tx.hash('hex'); + for (j = 0; j < tx.outputs.length; j++) { coin = this.getCoin(hash, j); if (coin) @@ -1291,8 +1296,10 @@ Mempool.prototype.getHistory = function getHistory() { for (i = 0; i < hashes.length; i++) { hash = hashes[i]; tx = this.getTX(hash); + if (!tx) continue; + txs.push(tx); } @@ -1313,7 +1320,7 @@ Mempool.prototype.getOrphan = function getOrphan(hash) { return; try { - orphan = bcoin.tx.fromExtended(data, true); + orphan = TX.fromExtended(data, true); } catch (e) { delete this.orphans[hash]; this.logger.warning('%s %s', @@ -1425,16 +1432,16 @@ Mempool.prototype.removeOrphan = function removeOrphan(tx) { * except that it will also fill with coins * from the blockchain as well. * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Mempool.prototype.fillAllHistory = function fillAllHistory(tx, callback) { +Mempool.prototype.fillAllHistory = function fillAllHistory(tx) { this.fillHistory(tx); if (tx.hasCoins()) - return callback(null, tx); + return Promise.resolve(tx); - this.chain.db.fillCoins(tx, callback); + return this.chain.db.fillCoins(tx); }; /** @@ -1443,42 +1450,33 @@ Mempool.prototype.fillAllHistory = function fillAllHistory(tx, callback) { * except that it will also fill with coins * from the blockchain as well. * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Mempool.prototype.fillAllCoins = function fillAllCoins(tx, callback) { - var self = this; +Mempool.prototype.fillAllCoins = co(function* fillAllCoins(tx) { + var i, input, hash, index, coin; this.fillCoins(tx); if (tx.hasCoins()) - return callback(null, tx); + return tx; - utils.forEachSerial(tx.inputs, function(input, next) { - var hash = input.prevout.hash; - var index = input.prevout.index; + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + hash = input.prevout.hash; + index = input.prevout.index; - if (self.isSpent(hash, index)) - return next(); + if (this.isSpent(hash, index)) + continue; - self.chain.db.getCoin(hash, index, function(err, coin) { - if (err) - return next(err); - - if (!coin) - return next(); + coin = yield this.chain.db.getCoin(hash, index); + if (coin) input.coin = coin; + } - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, tx); - }); -}; + return tx; +}); /** * Get a snapshot of all transaction hashes in the mempool. Used @@ -1494,11 +1492,11 @@ Mempool.prototype.getSnapshot = function getSnapshot() { * Check sequence locks on a transaction against the current tip. * @param {TX} tx * @param {LockFlags} flags - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Mempool.prototype.checkLocks = function checkLocks(tx, flags, callback) { - return this.chain.checkLocks(this.chain.tip, tx, flags, callback); +Mempool.prototype.checkLocks = function checkLocks(tx, flags) { + return this.chain.checkLocks(this.chain.tip, tx, flags); }; /** @@ -1508,7 +1506,7 @@ Mempool.prototype.checkLocks = function checkLocks(tx, flags, callback) { * the blockchain does not maintain a spent list. The transaction will * be seen as an orphan rather than a double spend. * @param {TX} tx - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ Mempool.prototype.isDoubleSpend = function isDoubleSpend(tx) { @@ -1528,15 +1526,13 @@ Mempool.prototype.isDoubleSpend = function isDoubleSpend(tx) { * Calculate bitcoinj-style confidence. * @see http://bit.ly/1OVQwlO * @param {TX|Hash} hash - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ -Mempool.prototype.getConfidence = function getConfidence(hash, callback) { - var tx; +Mempool.prototype.getConfidence = co(function* getConfidence(hash) { + var tx, result; - callback = utils.asyncify(callback); - - if (hash instanceof bcoin.tx) { + if (hash instanceof TX) { tx = hash; hash = hash.hash('hex'); } else { @@ -1544,33 +1540,25 @@ Mempool.prototype.getConfidence = function getConfidence(hash, callback) { } if (this.hasTX(hash)) - return callback(null, constants.confidence.PENDING); + return constants.confidence.PENDING; if (tx && this.isDoubleSpend(tx)) - return callback(null, constants.confidence.INCONFLICT); + return constants.confidence.INCONFLICT; if (tx && tx.block) { - return this.chain.db.isMainChain(tx.block, function(err, result) { - if (err) - return callback(err); - - if (result) - return callback(null, constants.confidence.BUILDING); - - callback(null, constants.confidence.DEAD); - }); + result = yield this.chain.db.isMainChain(tx.block); + if (result) + return constants.confidence.BUILDING; + return constants.confidence.DEAD; } - this.chain.db.hasCoins(hash, function(err, existing) { - if (err) - return callback(err); + result = yield this.chain.db.hasCoins(hash); - if (existing) - return callback(null, constants.confidence.BUILDING); + if (result) + return constants.confidence.BUILDING; - callback(null, constants.confidence.UNKNOWN); - }); -}; + return constants.confidence.UNKNOWN; +}); /** * Map a transaction to the mempool. @@ -1609,7 +1597,7 @@ Mempool.prototype.trackEntry = function trackEntry(entry) { if (output.script.isUnspendable()) continue; - coin = bcoin.coin.fromTX(tx, i); + coin = Coin.fromTX(tx, i); this.coinIndex.addCoin(coin); } } @@ -1653,7 +1641,7 @@ Mempool.prototype.untrackEntry = function untrackEntry(entry) { if (output.script.isUnspendable()) continue; - coin = bcoin.coin.fromTX(tx, i); + coin = Coin.fromTX(tx, i); this.coinIndex.removeCoin(coin); } } @@ -1667,7 +1655,7 @@ Mempool.prototype.untrackEntry = function untrackEntry(entry) { * @private * @param {MempoolEntry} entry * @param {Boolean} limit - * @param {Function} callback + * @returns {Promise} */ Mempool.prototype.removeSpenders = function removeSpenders(entry) { @@ -1779,243 +1767,6 @@ Mempool.prototype.getSize = function getSize() { return this.size; }; -/** - * Represents a mempool entry. - * @exports MempoolEntry - * @constructor - * @param {Object} options - * @param {TX} options.tx - Transaction in mempool. - * @param {Number} options.height - Entry height. - * @param {Number} options.priority - Entry priority. - * @param {Number} options.ts - Entry time. - * @param {Amount} options.chainValue - Value of on-chain coins. - * @param {Number} options.count - Number of descendants (includes tx). - * @param {Number} options.size - TX and descendant modified size. - * @param {Amount} options.fees - TX and descendant delta-applied fees. - * @property {TX} tx - * @property {Number} height - * @property {Number} priority - * @property {Number} ts - * @property {Amount} chainValue - * @property {Number} count - * @property {Number} size - * @property {Amount} fees - */ - -function MempoolEntry(options) { - if (!(this instanceof MempoolEntry)) - return new MempoolEntry(options); - - this.tx = null; - this.height = -1; - this.size = 0; - this.priority = 0; - this.fee = 0; - this.ts = 0; - - this.chainValue = 0; - this.count = 0; - this.sizes = 0; - this.fees = 0; - this.dependencies = false; - - if (options) - this.fromOptions(options); -} - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -MempoolEntry.prototype.fromOptions = function fromOptions(options) { - this.tx = options.tx; - this.height = options.height; - this.size = options.size; - this.priority = options.priority; - this.fee = options.fee; - this.ts = options.ts; - - this.chainValue = options.chainValue; - this.count = options.count; - this.sizes = options.sizes; - this.fees = options.fees; - this.dependencies = options.dependencies; - - return this; -}; - -/** - * Instantiate mempool entry from options. - * @param {Object} options - * @returns {MempoolEntry} - */ - -MempoolEntry.fromOptions = function fromOptions(options) { - return new MempoolEntry().fromOptions(options); -}; - -/** - * Inject properties from transaction. - * @private - * @param {TX} tx - * @param {Number} height - */ - -MempoolEntry.prototype.fromTX = function fromTX(tx, height) { - var priority = tx.getPriority(height); - var value = tx.getChainValue(height); - var dependencies = false; - var size = tx.getVirtualSize(); - var fee = tx.getFee(); - var i; - - for (i = 0; i < tx.inputs.length; i++) { - if (tx.inputs[i].coin.height === -1) { - dependencies = true; - break; - } - } - - this.tx = tx; - this.height = height; - this.size = size; - this.priority = priority; - this.fee = fee; - this.chainValue = value; - this.ts = utils.now(); - this.count = 1; - this.sizes = size; - this.fees = fee; - this.dependencies = dependencies; - - return this; -}; - -/** - * Create a mempool entry from a TX. - * @param {TX} tx - * @param {Number} height - Entry height. - * @returns {MempoolEntry} - */ - -MempoolEntry.fromTX = function fromTX(tx, height) { - return new MempoolEntry().fromTX(tx, height); -}; - -/** - * Serialize a mempool entry. Note that this - * can still be parsed as a regular tx since - * the mempool entry data comes after the - * serialized transaction. - * @param {BufferWriter?} writer - * @returns {Buffer} - */ - -MempoolEntry.prototype.toRaw = function toRaw(writer) { - var p = new BufferWriter(writer); - - this.tx.toRaw(p); - - p.writeU32(this.height); - p.writeU32(this.size); - p.writeDouble(this.priority); - p.writeVarint(this.fee); - p.writeVarint(this.chainValue); - p.writeU32(this.ts); - p.writeU32(this.count); - p.writeU32(this.sizes); - p.writeVarint(this.fees); - p.writeU8(this.dependencies ? 1 : 0); - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} data - */ - -MempoolEntry.prototype.fromRaw = function fromRaw(data) { - var p = new BufferReader(data); - this.tx = bcoin.tx.fromRaw(p); - this.height = p.readU32(); - this.size = p.readU32(); - this.priority = p.readDouble(); - this.fee = p.readVarint(); - this.chainValue = p.readVarint(); - this.ts = p.readU32(); - this.count = p.readU32(); - this.sizes = p.readU32(); - this.fees = p.readVarint(); - this.dependencies = p.readU8() === 1; - return this; -}; - -/** - * Create a mempool entry from serialized data. - * @param {Buffer|BufferReader} data - * @returns {MempoolEntry} - */ - -MempoolEntry.fromRaw = function fromRaw(data) { - return new MempoolEntry().fromRaw(data); -}; - -/** - * Calculate priority, taking into account - * the entry height delta, modified size, - * and chain value. - * @param {Number} height - * @returns {Number} Priority. - */ - -MempoolEntry.prototype.getPriority = function getPriority(height) { - var heightDelta = height - this.height; - var modSize = this.tx.getModifiedSize(this.size); - var deltaPriority = (heightDelta * this.chainValue) / modSize; - var result = this.priority + Math.floor(deltaPriority); - if (result < 0) - result = 0; - return result; -}; - -/** - * Get fee. - * @returns {Amount} - */ - -MempoolEntry.prototype.getFee = function getFee() { - return this.fee; -}; - -/** - * Calculate fee rate. - * @returns {Rate} - */ - -MempoolEntry.prototype.getRate = function getRate() { - return bcoin.tx.getRate(this.size, this.fee); -}; - -/** - * Test whether the entry is free with - * the current priority (calculated by - * current height). - * @param {Number} height - * @returns {Boolean} - */ - -MempoolEntry.prototype.isFree = function isFree(height) { - var priority = this.getPriority(height); - return priority > constants.tx.FREE_THRESHOLD; -}; - /** * Address Index */ @@ -2035,7 +1786,7 @@ AddressIndex.prototype.getCoins = function getCoins(address) { for (i = 0; i < items.length; i++) { item = items[i]; - outpoint = bcoin.outpoint.fromRaw(item); + outpoint = Outpoint.fromRaw(item); coin = this.mempool.getCoin(outpoint.hash, outpoint.index); assert(coin); out.push(coin); @@ -2121,7 +1872,7 @@ AddressIndex.prototype.addCoin = function addCoin(coin) { this.map[hash] = items; } - outpoint = bcoin.outpoint(coin.hash, coin.index).toRaw(); + outpoint = Outpoint(coin.hash, coin.index).toRaw(); utils.binaryInsert(items, outpoint, utils.cmp); this.map[key] = hash; @@ -2140,7 +1891,7 @@ AddressIndex.prototype.removeCoin = function removeCoin(coin) { if (!items) return; - outpoint = bcoin.outpoint(coin.hash, coin.index).toRaw(); + outpoint = Outpoint(coin.hash, coin.index).toRaw(); utils.binaryRemove(items, outpoint, utils.cmp); if (items.length === 0) @@ -2153,7 +1904,4 @@ AddressIndex.prototype.removeCoin = function removeCoin(coin) { * Expose */ -exports = Mempool; -exports.MempoolEntry = MempoolEntry; - -module.exports = exports; +module.exports = Mempool; diff --git a/lib/mempool/mempoolentry.js b/lib/mempool/mempoolentry.js new file mode 100644 index 00000000..d2ee2efa --- /dev/null +++ b/lib/mempool/mempoolentry.js @@ -0,0 +1,191 @@ +/*! + * mempoolentry.js - mempool entry object for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var constants = require('../protocol/constants'); +var utils = require('../utils/utils'); +var TX = require('../primitives/tx'); + +/** + * Represents a mempool entry. + * @exports MempoolEntry + * @constructor + * @param {Object} options + * @param {TX} options.tx - Transaction in mempool. + * @param {Number} options.height - Entry height. + * @param {Number} options.priority - Entry priority. + * @param {Number} options.ts - Entry time. + * @param {Amount} options.chainValue - Value of on-chain coins. + * @param {Number} options.count - Number of descendants (includes tx). + * @param {Number} options.size - TX and descendant modified size. + * @param {Amount} options.fees - TX and descendant delta-applied fees. + * @property {TX} tx + * @property {Number} height + * @property {Number} priority + * @property {Number} ts + * @property {Amount} chainValue + * @property {Number} count + * @property {Number} size + * @property {Amount} fees + */ + +function MempoolEntry(options) { + if (!(this instanceof MempoolEntry)) + return new MempoolEntry(options); + + this.tx = null; + this.height = -1; + this.size = 0; + this.priority = 0; + this.fee = 0; + this.ts = 0; + + this.chainValue = 0; + this.count = 0; + this.sizes = 0; + this.fees = 0; + this.dependencies = false; + + if (options) + this.fromOptions(options); +} + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +MempoolEntry.prototype.fromOptions = function fromOptions(options) { + this.tx = options.tx; + this.height = options.height; + this.size = options.size; + this.priority = options.priority; + this.fee = options.fee; + this.ts = options.ts; + + this.chainValue = options.chainValue; + this.count = options.count; + this.sizes = options.sizes; + this.fees = options.fees; + this.dependencies = options.dependencies; + + return this; +}; + +/** + * Instantiate mempool entry from options. + * @param {Object} options + * @returns {MempoolEntry} + */ + +MempoolEntry.fromOptions = function fromOptions(options) { + return new MempoolEntry().fromOptions(options); +}; + +/** + * Inject properties from transaction. + * @private + * @param {TX} tx + * @param {Number} height + */ + +MempoolEntry.prototype.fromTX = function fromTX(tx, height) { + var priority = tx.getPriority(height); + var value = tx.getChainValue(height); + var dependencies = false; + var size = tx.getVirtualSize(); + var fee = tx.getFee(); + var i; + + for (i = 0; i < tx.inputs.length; i++) { + if (tx.inputs[i].coin.height === -1) { + dependencies = true; + break; + } + } + + this.tx = tx; + this.height = height; + this.size = size; + this.priority = priority; + this.fee = fee; + this.chainValue = value; + this.ts = utils.now(); + this.count = 1; + this.sizes = size; + this.fees = fee; + this.dependencies = dependencies; + + return this; +}; + +/** + * Create a mempool entry from a TX. + * @param {TX} tx + * @param {Number} height - Entry height. + * @returns {MempoolEntry} + */ + +MempoolEntry.fromTX = function fromTX(tx, height) { + return new MempoolEntry().fromTX(tx, height); +}; + +/** + * Calculate priority, taking into account + * the entry height delta, modified size, + * and chain value. + * @param {Number} height + * @returns {Number} Priority. + */ + +MempoolEntry.prototype.getPriority = function getPriority(height) { + var heightDelta = height - this.height; + var modSize = this.tx.getModifiedSize(this.size); + var deltaPriority = (heightDelta * this.chainValue) / modSize; + var result = this.priority + Math.floor(deltaPriority); + if (result < 0) + result = 0; + return result; +}; + +/** + * Get fee. + * @returns {Amount} + */ + +MempoolEntry.prototype.getFee = function getFee() { + return this.fee; +}; + +/** + * Calculate fee rate. + * @returns {Rate} + */ + +MempoolEntry.prototype.getRate = function getRate() { + return TX.getRate(this.size, this.fee); +}; + +/** + * Test whether the entry is free with + * the current priority (calculated by + * current height). + * @param {Number} height + * @returns {Boolean} + */ + +MempoolEntry.prototype.isFree = function isFree(height) { + var priority = this.getPriority(height); + return priority > constants.tx.FREE_THRESHOLD; +}; + +/* + * Expose + */ + +module.exports = MempoolEntry; diff --git a/lib/miner/index.js b/lib/miner/index.js new file mode 100644 index 00000000..7779ae5d --- /dev/null +++ b/lib/miner/index.js @@ -0,0 +1,4 @@ +'use strict'; + +exports.Miner = require('./miner'); +exports.MinerBlock = require('./minerblock'); diff --git a/lib/miner/miner.js b/lib/miner/miner.js index 58bc83f4..682f6e46 100644 --- a/lib/miner/miner.js +++ b/lib/miner/miner.js @@ -7,11 +7,14 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); -var assert = utils.assert; +var co = require('../utils/co'); +var assert = require('assert'); var AsyncObject = require('../utils/async'); var MinerBlock = require('./minerblock'); +var Address = require('../primitives/address'); +var Workers = require('../workers/workers'); +var time = require('../net/timedata'); /** * A bitcoin miner (supports mining witness blocks). @@ -36,7 +39,7 @@ function Miner(options) { options = {}; this.options = options; - this.address = bcoin.address(this.options.address); + this.address = Address(this.options.address); this.coinbaseFlags = this.options.coinbaseFlags || 'mined by bcoin'; this.version = null; @@ -111,9 +114,9 @@ Miner.prototype._init = function _init() { stat.best); }); - if (bcoin.useWorkers) { - this.workerPool = new bcoin.workers({ - size: this.options.parallel ? 2 : 1, + if (Workers.enabled) { + this.workerPool = new Workers({ + size: 1, timeout: -1 }); @@ -130,38 +133,27 @@ Miner.prototype._init = function _init() { /** * Open the miner, wait for the chain and mempool to load. * @alias Miner#open - * @param {Function} callback + * @returns {Promise} */ -Miner.prototype._open = function open(callback) { - var self = this; +Miner.prototype._open = co(function* open() { + if (this.mempool) + yield this.mempool.open(); + else + yield this.chain.open(); - function open(callback) { - if (self.mempool) - self.mempool.open(callback); - else - self.chain.open(callback); - } - - open(function(err) { - if (err) - return callback(err); - - self.logger.info('Miner loaded (flags=%s).', - self.coinbaseFlags.toString('utf8')); - - callback(); - }); -}; + this.logger.info('Miner loaded (flags=%s).', + this.coinbaseFlags.toString('utf8')); +}); /** * Close the miner. * @alias Miner#close - * @param {Function} callback + * @returns {Promise} */ -Miner.prototype._close = function close(callback) { - callback(); +Miner.prototype._close = function close() { + return Promise.resolve(null); }; /** @@ -169,53 +161,57 @@ Miner.prototype._close = function close(callback) { * @param {Number?} version - Custom block version. */ -Miner.prototype.start = function start() { +Miner.prototype.start = co(function* start() { var self = this; + var attempt, block; this.stop(); this.running = true; // Create a new block and start hashing - this.createBlock(function(err, attempt) { - if (err) - return self.emit('error', err); + try { + attempt = yield this.createBlock(); + } catch (e) { + this.emit('error', e); + return; + } - if (!self.running) - return; + if (!this.running) + return; - self.attempt = attempt; + this.attempt = attempt; - attempt.on('status', function(status) { - self.emit('status', status); - }); - - attempt.mineAsync(function(err, block) { - if (err) { - if (!self.running) - return; - self.emit('error', err); - return self.start(); - } - - // Add our block to the chain - self.chain.add(block, function(err) { - if (err) { - if (err.type === 'VerifyError') - self.logger.warning('%s could not be added to chain.', block.rhash); - self.emit('error', err); - return self.start(); - } - - // Emit our newly found block - self.emit('block', block); - - // `tip` will now be emitted by chain - // and the whole process starts over. - }); - }); + attempt.on('status', function(status) { + self.emit('status', status); }); -}; + + try { + block = yield attempt.mineAsync(); + } catch (e) { + if (!this.running) + return; + this.emit('error', e); + return this.start(); + } + + // Add our block to the chain + try { + yield this.chain.add(block); + } catch (err) { + if (err.type === 'VerifyError') + this.logger.warning('%s could not be added to chain.', block.rhash); + this.emit('error', err); + this.start(); + return; + } + + // Emit our newly found block + this.emit('block', block); + + // `tip` will now be emitted by chain + // and the whole process starts over. +}); /** * Stop mining. @@ -239,97 +235,67 @@ Miner.prototype.stop = function stop() { /** * Create a block "attempt". * @param {Number?} version - Custom block version. - * @param {Function} callback - Returns [Error, {@link MinerBlock}]. + * @returns {Promise} - Returns {@link MinerBlock}. */ -Miner.prototype.createBlock = function createBlock(tip, callback) { - var self = this; - var i, ts, attempt, txs, tx; +Miner.prototype.createBlock = co(function* createBlock(tip) { + var i, ts, attempt, txs, tx, target, version; - if (typeof tip === 'function') { - callback = tip; - tip = null; - } + if (!this.loaded) + yield this.open(); if (!tip) tip = this.chain.tip; - ts = Math.max(bcoin.now(), tip.ts + 1); - - function computeVersion(callback) { - if (self.version != null) - return callback(null, self.version); - self.chain.computeBlockVersion(tip, callback); - } - - if (!this.loaded) { - this.open(function(err) { - if (err) - return callback(err); - self.createBlock(tip, callback); - }); - return; - } - assert(tip); + ts = Math.max(time.now(), tip.ts + 1); + // Find target - this.chain.getTargetAsync(ts, tip, function(err, target) { - if (err) - return callback(err); + target = yield this.chain.getTargetAsync(ts, tip); + if (this.version != null) { + version = this.version; + } else { // Calculate version with versionbits - computeVersion(function(err, version) { - if (err) - return callback(err); + version = yield this.chain.computeBlockVersion(tip); + } - attempt = new MinerBlock({ - workerPool: self.workerPool, - tip: tip, - version: version, - target: target, - address: self.address, - coinbaseFlags: self.coinbaseFlags, - witness: self.chain.segwitActive, - parallel: self.options.parallel, - network: self.network - }); - - if (!self.mempool) - return callback(null, attempt); - - txs = self.mempool.getHistory(); - - for (i = 0; i < txs.length; i++) { - tx = txs[i]; - attempt.addTX(tx); - } - - callback(null, attempt); - }); + attempt = new MinerBlock({ + workerPool: this.workerPool, + tip: tip, + version: version, + target: target, + address: this.address, + coinbaseFlags: this.coinbaseFlags, + witness: this.chain.segwitActive, + network: this.network }); -}; + + if (!this.mempool) + return attempt; + + txs = this.mempool.getHistory(); + + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + attempt.addTX(tx); + } + + return attempt; +}); /** * Mine a single block. * @param {Number?} version - Custom block version. - * @param {Function} callback - Returns [Error, [{@link Block}]]. + * @returns {Promise} - Returns [{@link Block}]. */ -Miner.prototype.mineBlock = function mineBlock(tip, callback) { - if (typeof tip === 'function') { - callback = tip; - tip = null; - } - +Miner.prototype.mineBlock = co(function* mineBlock(tip) { // Create a new block and start hashing - this.createBlock(tip, function(err, attempt) { - if (err) - return callback(err); - - attempt.mineAsync(callback); - }); -}; + var attempt = yield this.createBlock(tip); + return yield attempt.mineAsync(); +}); /* * Expose diff --git a/lib/miner/minerblock.js b/lib/miner/minerblock.js index 7850c21e..6f6edaf3 100644 --- a/lib/miner/minerblock.js +++ b/lib/miner/minerblock.js @@ -7,15 +7,23 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); +var co = require('../utils/co'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var Network = require('../protocol/network'); var bn = require('bn.js'); var EventEmitter = require('events').EventEmitter; var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); +var TX = require('../primitives/tx'); +var Address = require('../primitives/address'); +var Block = require('../primitives/block'); +var Input = require('../primitives/input'); +var Output = require('../primitives/output'); +var time = require('../net/timedata'); +var ChainEntry = require('../chain/chainentry'); /** * MinerBlock @@ -53,17 +61,16 @@ function MinerBlock(options) { this.coinbaseFlags = options.coinbaseFlags; this.witness = options.witness; this.address = options.address; - this.network = bcoin.network.get(options.network); + this.network = Network.get(options.network); this.timeout = null; - this.callback = null; if (typeof this.coinbaseFlags === 'string') this.coinbaseFlags = new Buffer(this.coinbaseFlags, 'utf8'); - this.coinbase = new bcoin.tx(); + this.coinbase = new TX(); this.coinbase.mutable = true; - this.block = new bcoin.block(); + this.block = new Block(); this.block.mutable = true; this._init(); @@ -83,7 +90,7 @@ MinerBlock.prototype._init = function _init() { var i, input, output, hash, witnessNonce; // Coinbase input. - input = new bcoin.input(); + input = new Input(); // Height (required in v2+ blocks) input.script.set(0, new bn(this.height)); @@ -108,7 +115,7 @@ MinerBlock.prototype._init = function _init() { cb.inputs.push(input); // Reward output. - output = new bcoin.output(); + output = new Output(); output.script.fromAddress(this.address); cb.outputs.push(output); @@ -126,14 +133,14 @@ MinerBlock.prototype._init = function _init() { input.witness.compile(); // Commitment output. - cb.outputs.push(new bcoin.output()); + cb.outputs.push(new Output()); } // Setup our block. block.version = options.version; block.prevBlock = this.tip.hash; block.merkleRoot = constants.NULL_HASH; - block.ts = Math.max(bcoin.now(), this.tip.ts + 1); + block.ts = Math.max(time.now(), this.tip.ts + 1); block.bits = this.bits; block.nonce = 0; block.height = this.height; @@ -190,7 +197,7 @@ MinerBlock.prototype.updateCoinbase = function updateCoinbase() { */ MinerBlock.prototype.updateNonce = function updateNonce() { - this.block.ts = Math.max(bcoin.now(), this.tip.ts + 1); + this.block.ts = Math.max(time.now(), this.tip.ts + 1); // Overflow the nonce and increment the extraNonce. this.block.nonce = 0; @@ -216,7 +223,7 @@ MinerBlock.prototype.updateMerkle = function updateMerkle() { this.updateCommitment(); // Update timestamp. - this.block.ts = Math.max(bcoin.now(), this.tip.ts + 1); + this.block.ts = Math.max(time.now(), this.tip.ts + 1); // Recalculate merkle root. this.block.merkleRoot = this.block.getMerkleRoot('hex'); @@ -306,7 +313,7 @@ MinerBlock.prototype.findNonce = function findNonce() { // update the timestamp. This improves // performance because we do not have to // recalculate the merkle root. - now = bcoin.now(); + now = time.now(); if (now > block.ts && now > tip.ts) { block.ts = now; // Overflow the nonce @@ -346,19 +353,34 @@ MinerBlock.prototype.sendStatus = function sendStatus() { /** * Mine until the block is found. Will take a breather * for 100ms every time the nonce overflows. - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -MinerBlock.prototype.mine = function mine(callback) { +MinerBlock.prototype.mine = co(function* mine() { + yield this.wait(100); + + // Try to find a block: do one iteration of extraNonce + if (!this.findNonce()) { + yield this.mine(); + return; + } + + return this.block; +}); + +/** + * Wait for a timeout. + * @param {Number} time + * @returns {Promise} + */ + +MinerBlock.prototype.wait = function wait(time) { var self = this; - - this.timeout = setTimeout(function() { - // Try to find a block: do one iteration of extraNonce - if (!self.findNonce()) - return self.mine(callback); - - callback(null, self.block); - }, 100); + return new Promise(function(resolve, reject) { + self.timeout = setTimeout(function() { + resolve(); + }, time); + }); }; /** @@ -373,33 +395,21 @@ MinerBlock.prototype.mineSync = function mineSync() { /** * Attempt to mine the block on the worker pool. - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -MinerBlock.prototype.mineAsync = function mine(callback) { - var self = this; +MinerBlock.prototype.mineAsync = co(function* mineAsync() { + var block; if (!this.workerPool) - return this.mine(callback); + return yield this.mine(); - callback = utils.once(callback); + block = yield this.workerPool.mine(this); - this.callback = callback; + this.workerPool.destroy(); - function done(err, block) { - self.workerPool.destroy(); - callback(err, block); - } - - if (this.options.parallel) { - done = utils.once(done); - this.workerPool.mine(this, done); - this.workerPool.mine(this, done); - return; - } - - this.workerPool.mine(this, callback); -}; + return block; +}); /** * Destroy the minerblock. Stop mining. Clear timeout. @@ -410,10 +420,6 @@ MinerBlock.prototype.destroy = function destroy() { clearTimeout(this.timeout); this.timeout = null; } - if (this.callback) { - this.callback(new Error('Destroyed.')); - this.callback = null; - } this.block = null; }; @@ -452,11 +458,11 @@ MinerBlock.prototype.toRaw = function toRaw(writer) { MinerBlock.fromRaw = function fromRaw(data) { var p = new BufferReader(data); - var network = bcoin.network.fromMagic(p.readU32()); - var tip = bcoin.chainentry.fromRaw(null, p); + var network = Network.fromMagic(p.readU32()); + var tip = ChainEntry.fromRaw(null, p); var version = p.readU32(); var bits = p.readU32(); - var address = bcoin.address.fromRaw(p.readVarBytes()); + var address = Address.fromRaw(p.readVarBytes()); var coinbaseFlags = p.readVarBytes(); var witness = p.readU8() === 1; var count = p.readVarint(); @@ -464,7 +470,7 @@ MinerBlock.fromRaw = function fromRaw(data) { var i; for (i = 0; i < count; i++) - txs.push(bcoin.tx.fromRaw(p)); + txs.push(TX.fromRaw(p)); tip.network = network; diff --git a/lib/net/bip150.js b/lib/net/bip150.js index b414e9ed..20d664f5 100644 --- a/lib/net/bip150.js +++ b/lib/net/bip150.js @@ -9,12 +9,14 @@ 'use strict'; var EventEmitter = require('events').EventEmitter; -var bcoin = require('../env'); var utils = require('../utils/utils'); +var co = require('../utils/co'); var crypto = require('../crypto/crypto'); var packets = require('./packets'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var ec = require('../crypto/ec'); +var BufferWriter = require('../utils/writer'); /** * Represents a BIP150 input and output stream. @@ -54,7 +56,7 @@ function BIP150(bip151, hostname, outbound, db, identity) { // Identity keypair this.privateKey = identity; - this.publicKey = bcoin.ec.publicKeyCreate(identity, true); + this.publicKey = ec.publicKeyCreate(identity, true); this.challengeReceived = false; this.replyReceived = false; @@ -95,10 +97,10 @@ BIP150.prototype.challenge = function challenge(hash) { this.emit('auth'); } - sig = bcoin.ec.sign(msg, this.privateKey); + sig = ec.sign(msg, this.privateKey); // authreply - return bcoin.ec.fromDER(sig); + return ec.fromDER(sig); }; BIP150.prototype.reply = function reply(data) { @@ -115,10 +117,10 @@ BIP150.prototype.reply = function reply(data) { if (!this.peerIdentity) return crypto.randomBytes(32); - sig = bcoin.ec.toDER(data); + sig = ec.toDER(data); msg = this.hash(this.output.sid, type, this.peerIdentity); - result = bcoin.ec.verify(msg, sig, this.peerIdentity); + result = ec.verify(msg, sig, this.peerIdentity); if (!result) return crypto.randomBytes(32); @@ -247,7 +249,14 @@ BIP150.prototype.complete = function complete(err) { this.callback = null; }; -BIP150.prototype.wait = function wait(timeout, callback) { +BIP150.prototype.wait = function wait(timeout) { + var self = this; + return new Promise(function(resolve, reject) { + self._wait(timeout, co.wrap(resolve, reject)); + }); +}; + +BIP150.prototype._wait = function wait(timeout, callback) { var self = this; assert(!this.auth, 'Cannot wait for init after handshake.'); @@ -272,7 +281,7 @@ BIP150.prototype.getAddress = function getAddress() { }; BIP150.address = function address(key) { - var p = new bcoin.writer(); + var p = new BufferWriter(); p.writeU8(0x0f); p.writeU16BE(0xff01); p.writeBytes(crypto.hash160(key)); diff --git a/lib/net/bip151.js b/lib/net/bip151.js index 00d39e87..21d2827b 100644 --- a/lib/net/bip151.js +++ b/lib/net/bip151.js @@ -13,13 +13,16 @@ 'use strict'; var EventEmitter = require('events').EventEmitter; -var bcoin = require('../env'); var utils = require('../utils/utils'); +var co = require('../utils/co'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); var chachapoly = require('../crypto/chachapoly'); var packets = require('./packets'); +var ec = require('../crypto/ec'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); var EncinitPacket = packets.EncinitPacket; var EncackPacket = packets.EncackPacket; @@ -58,7 +61,7 @@ function BIP151Stream(cipher) { return new BIP151Stream(cipher); this.cipher = cipher || 0; - this.privateKey = bcoin.ec.generatePrivateKey(); + this.privateKey = ec.generatePrivateKey(); this.publicKey = null; this.secret = null; this.prk = null; @@ -86,10 +89,10 @@ function BIP151Stream(cipher) { */ BIP151Stream.prototype.init = function init(publicKey) { - var p = bcoin.writer(); + var p = new BufferWriter(); this.publicKey = publicKey; - this.secret = bcoin.ec.ecdh(this.publicKey, this.privateKey); + this.secret = ec.ecdh(this.publicKey, this.privateKey); p.writeBytes(this.secret); p.writeU8(this.cipher); @@ -199,7 +202,7 @@ BIP151Stream.prototype.update = function update() { */ BIP151Stream.prototype.getPublicKey = function getPublicKey() { - return bcoin.ec.publicKeyCreate(this.privateKey, true); + return ec.publicKeyCreate(this.privateKey, true); }; /** @@ -452,10 +455,22 @@ BIP151.prototype.complete = function complete(err) { /** * Set a timeout and wait for handshake to complete. * @param {Number} timeout - Timeout in ms. - * @param {Function} callback + * @returns {Promise} */ -BIP151.prototype.wait = function wait(timeout, callback) { +BIP151.prototype.wait = function wait(timeout) { + var self = this; + return new Promise(function(resolve, reject) { + self._wait(timeout, co.wrap(resolve, reject)); + }); +}; + +/** + * Set a timeout and wait for handshake to complete. + * @private + */ + +BIP151.prototype._wait = function wait(timeout, callback) { var self = this; assert(!this.handshake, 'Cannot wait for init after handshake.'); @@ -510,7 +525,7 @@ BIP151.prototype.maybeRekey = function maybeRekey(data) { */ BIP151.prototype.packet = function packet(cmd, body) { - var p = bcoin.writer(); + var p = new BufferWriter(); var payload, packet; p.writeVarString(cmd, 'ascii'); @@ -652,7 +667,7 @@ BIP151.prototype.parse = function parse(data) { this.input.decrypt(payload); this.input.sequence(); - p = bcoin.reader(payload); + p = new BufferReader(payload); while (p.left()) { try { diff --git a/lib/net/bip152.js b/lib/net/bip152.js index 610f1610..3eb866fe 100644 --- a/lib/net/bip152.js +++ b/lib/net/bip152.js @@ -6,13 +6,17 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); +var BufferReader = require('../utils/reader'); +var BufferWriter = require('../utils/writer'); +var co = require('../utils/co'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); var siphash = require('../crypto/siphash'); -var AbstractBlock = bcoin.abstractblock; +var AbstractBlock = require('../primitives/abstractblock'); +var TX = require('../primitives/tx'); +var Block = require('../primitives/block'); /** * Represents a compact block (bip152): `cmpctblock` packet. @@ -39,6 +43,7 @@ function CompactBlock(options) { this.count = 0; this.sipKey = null; this.timeout = null; + this.callback = null; if (options) this.fromOptions(options); @@ -81,7 +86,7 @@ CompactBlock.fromOptions = function fromOptions(options) { }; CompactBlock.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = new BufferReader(data); var i, count, index, tx; this.version = p.readU32(); // Technically signed @@ -110,7 +115,7 @@ CompactBlock.prototype.fromRaw = function fromRaw(data) { index = p.readVarint(); assert(index <= 0xffff); assert(index < this.totalTX); - tx = bcoin.tx.fromRaw(p); + tx = TX.fromRaw(p); this.ptx.push([index, tx]); } @@ -134,10 +139,10 @@ CompactBlock.prototype.toNormal = function toNormal(writer) { }; CompactBlock.prototype.frame = function frame(witness, writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i, id, lo, hi, ptx; - p.write32(this.version); + p.writeU32(this.version); p.writeHash(this.prevBlock); p.writeHash(this.merkleRoot); p.writeU32(this.ts); @@ -306,7 +311,7 @@ CompactBlock.prototype.init = function init() { }; CompactBlock.prototype.toBlock = function toBlock() { - var block = new bcoin.block(); + var block = new Block(); var i, tx; block.version = this.version; @@ -362,12 +367,31 @@ CompactBlock.fromBlock = function fromBlock(block, nonce) { return new CompactBlock().fromBlock(block, nonce); }; -CompactBlock.prototype.startTimeout = function startTimeout(time, callback) { - assert(this.timeout == null); - this.timeout = setTimeout(callback, time); +CompactBlock.prototype.wait = function wait(time) { + var self = this; + return new Promise(function(resolve, reject) { + self._wait(time, co.wrap(resolve, reject)); + }); }; -CompactBlock.prototype.stopTimeout = function stopTimeout() { +CompactBlock.prototype._wait = function wait(time, callback) { + var self = this; + assert(this.timeout == null); + this.callback = callback; + this.timeout = setTimeout(function() { + self.complete(new Error('Timed out.')); + }, time); +}; + +CompactBlock.prototype.complete = function complete(err) { + if (this.timeout != null) { + clearTimeout(this.timeout); + this.timeout = null; + this.callback(err); + } +}; + +CompactBlock.prototype.destroy = function destroy() { if (this.timeout != null) { clearTimeout(this.timeout); this.timeout = null; @@ -422,7 +446,7 @@ TXRequest.fromCompact = function fromCompact(block) { }; TXRequest.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, count, index, offset; this.hash = p.readHash('hex'); @@ -453,7 +477,7 @@ TXRequest.fromRaw = function fromRaw(data) { }; TXRequest.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i, index; p.writeHash(this.hash); @@ -502,7 +526,7 @@ TXResponse.fromOptions = function fromOptions(options) { }; TXResponse.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, count; this.hash = p.readHash('hex'); @@ -510,7 +534,7 @@ TXResponse.prototype.fromRaw = function fromRaw(data) { count = p.readVarint(); for (i = 0; i < count; i++) - this.txs.push(bcoin.tx.fromRaw(p)); + this.txs.push(TX.fromRaw(p)); return this; }; @@ -547,7 +571,7 @@ TXResponse.prototype.toNormal = function toNormal(writer) { }; TXResponse.prototype.frame = function frame(witness, writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i, tx; p.writeHash(this.hash); diff --git a/lib/net/framer.js b/lib/net/framer.js index a3d4edb1..424fa97b 100644 --- a/lib/net/framer.js +++ b/lib/net/framer.js @@ -7,10 +7,9 @@ 'use strict'; -var bcoin = require('../env'); -var utils = require('../utils/utils'); +var Network = require('../protocol/network'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; +var assert = require('assert'); /** * Protocol packet framer @@ -28,7 +27,7 @@ function Framer(options) { this.options = options; - this.network = bcoin.network.get(options.network); + this.network = Network.get(options.network); this.bip151 = options.bip151; } diff --git a/lib/net/index.js b/lib/net/index.js new file mode 100644 index 00000000..0c072e06 --- /dev/null +++ b/lib/net/index.js @@ -0,0 +1,12 @@ +'use strict'; + +exports.bip150 = require('./bip150'); +exports.bip151 = require('./bip151'); +exports.bip152 = require('./bip152'); +exports.Framer = require('./framer'); +exports.packets = require('./packets'); +exports.Parser = require('./parser'); +exports.Peer = require('./peer'); +exports.Pool = require('./pool'); +exports.ProxySocket = require('./proxysocket'); +exports.time = require('./timedata'); diff --git a/lib/net/packets.js b/lib/net/packets.js index 175f927e..2bbd3c33 100644 --- a/lib/net/packets.js +++ b/lib/net/packets.js @@ -7,13 +7,26 @@ 'use strict'; -var bcoin = require('../env'); +var bn = require('bn.js'); var constants = require('../protocol/constants'); var utils = require('../utils/utils'); +var assert = require('assert'); var crypto = require('../crypto/crypto'); -var bn = require('bn.js'); +var time = require('./timedata'); +var ec = require('../crypto/ec'); +var Bloom = require('../utils/bloom'); +var bip152 = require('./bip152'); var NetworkAddress = require('../primitives/netaddress'); -var assert = utils.assert; +var Coin = require('../primitives/coin'); +var Headers = require('../primitives/headers'); +var InvItem = require('../primitives/invitem'); +var MemBlock = require('../primitives/memblock'); +var MerkleBlock = require('../primitives/merkleblock'); +var Outpoint = require('../primitives/outpoint'); +var Output = require('../primitives/output'); +var TX = require('../primitives/tx'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); var DUMMY = new Buffer(0); /** @@ -106,7 +119,7 @@ function VersionPacket(options) { this.version = constants.VERSION; this.services = constants.LOCAL_SERVICES; - this.ts = bcoin.now(); + this.ts = time.now(); this.recv = new NetworkAddress(); this.from = new NetworkAddress(); this.nonce = new bn(0); @@ -176,7 +189,7 @@ VersionPacket.fromOptions = function fromOptions(options) { */ VersionPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.write32(this.version); p.writeU64(this.services); @@ -256,7 +269,7 @@ VersionPacket.prototype.hasCompact = function hasCompact() { */ VersionPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.version = p.read32(); this.services = p.readU53(); @@ -376,7 +389,7 @@ PingPacket.prototype.type = exports.types.PING; */ PingPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); if (this.nonce) p.writeU64(this.nonce); @@ -394,7 +407,7 @@ PingPacket.prototype.toRaw = function toRaw(writer) { */ PingPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); if (p.left() >= 8) this.nonce = p.readU64(); return this; @@ -441,7 +454,7 @@ PongPacket.prototype.type = exports.types.PONG; */ PongPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeU64(this.nonce); @@ -458,7 +471,7 @@ PongPacket.prototype.toRaw = function toRaw(writer) { */ PongPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.nonce = p.readU64(); return this; }; @@ -498,18 +511,18 @@ PongPacket.fromRaw = function fromRaw(data, enc) { */ function AlertPacket(options) { - var time; + var ts; if (!(this instanceof AlertPacket)) return new AlertPacket(options); Packet.call(this); - time = bcoin.now() + 7 * 86400; + ts = time.now() + 7 * 86400; this.version = 1; - this.relayUntil = time; - this.expiration = time; + this.relayUntil = ts; + this.expiration = ts; this.id = 1; this.cancel = 0; this.cancels = []; @@ -625,7 +638,7 @@ AlertPacket.prototype.toPayload = function toPayload() { */ AlertPacket.prototype.sign = function sign(key) { - this.signature = bcoin.ec.sign(this.hash(), key); + this.signature = ec.sign(this.hash(), key); }; /** @@ -635,7 +648,7 @@ AlertPacket.prototype.sign = function sign(key) { */ AlertPacket.prototype.verify = function verify(key) { - return bcoin.ec.verify(this.hash(), this.signature, key); + return ec.verify(this.hash(), this.signature, key); }; /** @@ -644,7 +657,7 @@ AlertPacket.prototype.verify = function verify(key) { */ AlertPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeVarBytes(this.toPayload()); p.writeVarBytes(this.signature); @@ -662,7 +675,7 @@ AlertPacket.prototype.toRaw = function toRaw(writer) { */ AlertPacket.prototype.framePayload = function framePayload(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i; p.write32(this.version); @@ -700,13 +713,13 @@ AlertPacket.prototype.framePayload = function framePayload(writer) { */ AlertPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, count; this._payload = p.readVarBytes(); this.signature = p.readVarBytes(); - p = bcoin.reader(this._payload); + p = BufferReader(this._payload); this.version = p.read32(); this.relayUntil = p.read53(); @@ -824,7 +837,7 @@ AddrPacket.prototype.type = exports.types.ADDR; */ AddrPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i; p.writeVarint(this.items.length); @@ -845,7 +858,7 @@ AddrPacket.prototype.toRaw = function toRaw(writer) { */ AddrPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, count; count = p.readVarint(); @@ -897,7 +910,7 @@ InvPacket.prototype.type = exports.types.INV; */ InvPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i; p.writeVarint(this.items.length); @@ -918,13 +931,13 @@ InvPacket.prototype.toRaw = function toRaw(writer) { */ InvPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, count; count = p.readVarint(); for (i = 0; i < count; i++) - this.items.push(bcoin.invitem.fromRaw(p)); + this.items.push(InvItem.fromRaw(p)); return this; }; @@ -1040,7 +1053,7 @@ GetBlocksPacket.prototype.type = exports.types.GETBLOCKS; */ GetBlocksPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i; p.writeU32(this.version); @@ -1064,7 +1077,7 @@ GetBlocksPacket.prototype.toRaw = function toRaw(writer) { */ GetBlocksPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, count; this.version = p.readU32(); @@ -1157,7 +1170,7 @@ HeadersPacket.prototype.type = exports.types.HEADERS; */ HeadersPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i; p.writeVarint(this.items.length); @@ -1178,13 +1191,13 @@ HeadersPacket.prototype.toRaw = function toRaw(writer) { */ HeadersPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, count; count = p.readVarint(); for (i = 0; i < count; i++) - this.items.push(bcoin.headers.fromRaw(p)); + this.items.push(Headers.fromRaw(p)); return this; }; @@ -1268,7 +1281,7 @@ function BlockPacket(block, witness) { Packet.call(this); - this.block = block || new bcoin.memblock(); + this.block = block || new MemBlock(); this.witness = witness || false; } @@ -1328,7 +1341,7 @@ function TXPacket(tx, witness) { Packet.call(this); - this.tx = tx || new bcoin.tx(); + this.tx = tx || new TX(); this.witness = witness || false; } @@ -1450,7 +1463,7 @@ RejectPacket.fromOptions = function fromOptions(options) { */ RejectPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); assert(this.message.length <= 12); assert(this.reason.length <= 111); @@ -1475,7 +1488,7 @@ RejectPacket.prototype.toRaw = function toRaw(writer) { */ RejectPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.message = p.readVarString('ascii', 12); this.code = p.readU8(); @@ -1525,7 +1538,7 @@ RejectPacket.prototype.fromReason = function fromReason(code, reason, obj) { this.reason = reason; if (obj) { - this.message = (obj instanceof bcoin.tx) ? 'tx' : 'block'; + this.message = (obj instanceof TX) ? 'tx' : 'block'; this.data = obj.hash('hex'); } @@ -1639,10 +1652,14 @@ function FilterLoadPacket(filter, n, tweak, update) { Packet.call(this); - this.filter = filter || DUMMY; - this.n = n || 0; - this.tweak = tweak || 0; - this.update = update || 0; + if (filter instanceof Bloom) { + this.fromFilter(filter); + } else { + this.filter = filter || DUMMY; + this.n = n || 0; + this.tweak = tweak || 0; + this.update = update || 0; + } } utils.inherits(FilterLoadPacket, Packet); @@ -1656,7 +1673,7 @@ FilterLoadPacket.prototype.type = exports.types.FILTERLOAD; */ FilterLoadPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeVarBytes(this.filter); p.writeU32(this.n); @@ -1675,7 +1692,7 @@ FilterLoadPacket.prototype.toRaw = function toRaw(writer) { */ FilterLoadPacket.prototype.toFilter = function toFilter() { - return new bcoin.bloom(this.filter, this.n, this.tweak, this.update); + return new Bloom(this.filter, this.n, this.tweak, this.update); }; /** @@ -1685,7 +1702,7 @@ FilterLoadPacket.prototype.toFilter = function toFilter() { */ FilterLoadPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.filter = p.readVarBytes(); this.n = p.readU32(); @@ -1778,7 +1795,7 @@ FilterAddPacket.prototype.type = exports.types.FILTERADD; */ FilterAddPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeVarBytes(this.data); @@ -1795,7 +1812,7 @@ FilterAddPacket.prototype.toRaw = function toRaw(writer) { */ FilterAddPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.data = p.readVarBytes(); return this; }; @@ -1877,7 +1894,7 @@ function MerkleBlockPacket(block) { Packet.call(this); - this.block = block || new bcoin.merkleblock(); + this.block = block || new MerkleBlock(); } utils.inherits(MerkleBlockPacket, Packet); @@ -1949,7 +1966,7 @@ GetUTXOsPacket.prototype.type = exports.types.GETUTXOS; */ GetUTXOsPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i; p.writeU8(this.mempool ? 1 : 0); @@ -1971,7 +1988,7 @@ GetUTXOsPacket.prototype.toRaw = function toRaw(writer) { */ GetUTXOsPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, count; this.mempool = p.readU8() === 1; @@ -1979,7 +1996,7 @@ GetUTXOsPacket.prototype.fromRaw = function fromRaw(data) { count = p.readVarint(); for (i = 0; i < count; i++) - this.prevout.push(bcoin.outpoint.fromRaw(p)); + this.prevout.push(Outpoint.fromRaw(p)); return this; }; @@ -2075,7 +2092,7 @@ UTXOsPacket.fromOptions = function fromOptions(options) { */ UTXOsPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var map = new Buffer((this.hits.length + 7) / 8 | 0); var i, bit, oct, coin, height; @@ -2116,7 +2133,7 @@ UTXOsPacket.prototype.toRaw = function toRaw(writer) { */ UTXOsPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, bit, oct, coin, output; var version, height, map, count; @@ -2135,12 +2152,12 @@ UTXOsPacket.prototype.fromRaw = function fromRaw(data) { for (i = 0; i < count; i++) { version = p.readU32(); height = p.readU32(); - coin = new bcoin.coin(); + coin = new Coin(); if (height === 0x7fffffff) height = -1; - output = bcoin.output.fromRaw(p); + output = Output.fromRaw(p); coin.version = version; coin.height = height; @@ -2244,7 +2261,7 @@ FeeFilterPacket.prototype.type = exports.types.FEEFILTER; */ FeeFilterPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.write64(this.rate); @@ -2261,7 +2278,7 @@ FeeFilterPacket.prototype.toRaw = function toRaw(writer) { */ FeeFilterPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.rate = p.read64N(); return this; }; @@ -2310,7 +2327,7 @@ SendCmpctPacket.prototype.type = exports.types.SENDCMPCT; */ SendCmpctPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeU8(this.mode); p.writeU64(this.version); @@ -2328,7 +2345,7 @@ SendCmpctPacket.prototype.toRaw = function toRaw(writer) { */ SendCmpctPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.mode = p.readU8(); this.version = p.readU53(); return this; @@ -2363,7 +2380,7 @@ function CmpctBlockPacket(block, witness) { Packet.call(this); - this.block = block || new bcoin.bip152.CompactBlock(); + this.block = block || new bip152.CompactBlock(); this.witness = witness || false; } @@ -2421,7 +2438,7 @@ function GetBlockTxnPacket(request) { Packet.call(this); - this.request = request || new bcoin.bip152.TXRequest(); + this.request = request || new bip152.TXRequest(); } utils.inherits(GetBlockTxnPacket, Packet); @@ -2478,7 +2495,7 @@ function BlockTxnPacket(response, witness) { Packet.call(this); - this.response = response || new bcoin.bip152.TXResponse(); + this.response = response || new bip152.TXResponse(); this.witness = witness || false; } @@ -2553,7 +2570,7 @@ EncinitPacket.prototype.type = exports.types.ENCINIT; */ EncinitPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeBytes(this.publicKey); p.writeU8(this.cipher); @@ -2571,7 +2588,7 @@ EncinitPacket.prototype.toRaw = function toRaw(writer) { */ EncinitPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.publicKey = p.readBytes(33); this.cipher = p.readU8(); return this; @@ -2618,7 +2635,7 @@ EncackPacket.prototype.type = exports.types.ENCACK; */ EncackPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeBytes(this.publicKey); @@ -2635,7 +2652,7 @@ EncackPacket.prototype.toRaw = function toRaw(writer) { */ EncackPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.publicKey = p.readBytes(33); return this; }; @@ -2681,7 +2698,7 @@ AuthChallengePacket.prototype.type = exports.types.AUTHCHALLENGE; */ AuthChallengePacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeBytes(this.hash); @@ -2698,7 +2715,7 @@ AuthChallengePacket.prototype.toRaw = function toRaw(writer) { */ AuthChallengePacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.hash = p.readHash(); return this; }; @@ -2744,7 +2761,7 @@ AuthReplyPacket.prototype.type = exports.types.AUTHREPLY; */ AuthReplyPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeBytes(this.signature); @@ -2761,7 +2778,7 @@ AuthReplyPacket.prototype.toRaw = function toRaw(writer) { */ AuthReplyPacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.signature = p.readBytes(64); return this; }; @@ -2807,7 +2824,7 @@ AuthProposePacket.prototype.type = exports.types.AUTHPROPOSE; */ AuthProposePacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeBytes(this.hash); @@ -2824,7 +2841,7 @@ AuthProposePacket.prototype.toRaw = function toRaw(writer) { */ AuthProposePacket.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.hash = p.readHash(); return this; }; @@ -2872,7 +2889,7 @@ UnknownPacket.prototype.type = exports.types.UNKNOWN; */ UnknownPacket.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeBytes(this.data); diff --git a/lib/net/parser.js b/lib/net/parser.js index 2a4005af..455e5b8d 100644 --- a/lib/net/parser.js +++ b/lib/net/parser.js @@ -7,11 +7,11 @@ 'use strict'; -var bcoin = require('../env'); +var Network = require('../protocol/network'); var EventEmitter = require('events').EventEmitter; var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; +var assert = require('assert'); var constants = require('../protocol/constants'); var packets = require('./packets'); @@ -33,7 +33,7 @@ function Parser(options) { EventEmitter.call(this); - this.network = bcoin.network.get(options.network); + this.network = Network.get(options.network); this.bip151 = options.bip151; this.pending = []; diff --git a/lib/net/peer.js b/lib/net/peer.js index fb3e18dd..3a009662 100644 --- a/lib/net/peer.js +++ b/lib/net/peer.js @@ -7,17 +7,25 @@ 'use strict'; -var bcoin = require('../env'); var EventEmitter = require('events').EventEmitter; var utils = require('../utils/utils'); +var co = require('../utils/co'); var Parser = require('./parser'); var Framer = require('./framer'); var packets = require('./packets'); var packetTypes = packets.types; var NetworkAddress = require('../primitives/netaddress'); -var assert = utils.assert; -var constants = bcoin.constants; -var InvItem = bcoin.invitem; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var InvItem = require('../primitives/invitem'); +var Locker = require('../utils/locker'); +var Bloom = require('../utils/bloom'); +var BIP151 = require('./bip151'); +var BIP150 = require('./bip150'); +var BIP152 = require('./bip152'); +var Block = require('../primitives/block'); +var TX = require('../primitives/tx'); +var time = require('./timedata'); /** * Represents a remote peer. @@ -79,7 +87,7 @@ function Peer(pool, addr, socket) { this.chain = this.pool.chain; this.mempool = this.pool.mempool; this.network = this.chain.network; - this.locker = new bcoin.locker(this); + this.locker = new Locker(); this.version = null; this.destroyed = false; this.ack = false; @@ -91,8 +99,8 @@ function Peer(pool, addr, socket) { this.spvFilter = null; this.relay = true; this.feeRate = -1; - this.addrFilter = new bcoin.bloom.rolling(5000, 0.001); - this.invFilter = new bcoin.bloom.rolling(50000, 0.000001); + this.addrFilter = new Bloom.Rolling(5000, 0.001); + this.invFilter = new Bloom.Rolling(50000, 0.000001); this.lastBlock = null; this.waiting = 0; this.syncSent = false; @@ -104,6 +112,7 @@ function Peer(pool, addr, socket) { this.bip150 = null; this.lastSend = 0; this.lastRecv = 0; + this.outgoing = 0; this.challenge = null; this.lastPong = -1; @@ -141,9 +150,9 @@ function Peer(pool, addr, socket) { } if (this.options.bip151) { - this.bip151 = new bcoin.bip151(); + this.bip151 = new BIP151(); if (this.options.bip150) { - this.bip150 = new bcoin.bip150( + this.bip150 = new BIP150( this.bip151, this.hostname, this.outbound, @@ -177,10 +186,6 @@ Peer.uid = 0; Peer.prototype._init = function init() { var self = this; - this.socket.once('connect', function() { - self._onConnect(); - }); - this.socket.once('error', function(err) { self.error(err); @@ -207,9 +212,13 @@ Peer.prototype._init = function init() { self.parser.feed(chunk); }); - this.parser.on('packet', function(packet) { - self._onPacket(packet); - }); + this.parser.on('packet', co(function* (packet) { + try { + yield self._onPacket(packet); + } catch (e) { + self.error(e); + } + })); this.parser.on('error', function(err) { self.error(err, true); @@ -227,20 +236,96 @@ Peer.prototype._init = function init() { }); } - if (this.connected) { - utils.nextTick(function() { - self._onConnect(); - }); - } + this.open(); }; /** - * Invoke mutex lock. + * Create the socket and begin connecting. This method + * will use `options.createSocket` if provided. + * @param {String} host + * @param {Number} port + * @returns {net.Socket} + */ + +Peer.prototype.connect = function connect(port, host) { + var self = this; + var socket, proxy, net; + + assert(!this.socket); + + if (this.createSocket) { + socket = this.createSocket(port, host); + } else { + if (utils.isBrowser) { + proxy = require('./proxysocket'); + socket = proxy.connect(this.pool.proxyServer, port, host); + } else { + net = require('net'); + socket = net.connect(port, host); + } + } + + this.logger.debug('Connecting to %s.', this.hostname); + + socket.once('connect', function() { + self.logger.info('Connected to %s.', self.hostname); + }); + + return socket; +}; + +/** + * Open and initialize the peer. + */ + +Peer.prototype.open = co(function* open() { + try { + yield this._connect(); + yield this._bip151(); + yield this._bip150(); + yield this._handshake(); + yield this._finalize(); + } catch (e) { + this.error(e); + return; + } + + // Finally we can let the pool know + // that this peer is ready to go. + this.emit('open'); +}); + +/** + * Wait for connection. * @private */ -Peer.prototype._lock = function _lock(func, args, force) { - return this.locker.lock(func, args, force); +Peer.prototype._connect = function _connect() { + var self = this; + + if (this.connected) { + assert(!this.outbound); + return co.wait(); + } + + return new Promise(function(resolve, reject) { + self.socket.once('connect', function() { + self.ts = utils.now(); + self.connected = true; + self.emit('connect'); + + clearTimeout(self.connectTimeout); + self.connectTimeout = null; + + resolve(); + }); + + self.connectTimeout = setTimeout(function() { + self.connectTimeout = null; + reject(new Error('Connection timed out.')); + self.ignore(); + }, 10000); + }); }; /** @@ -249,96 +334,70 @@ Peer.prototype._lock = function _lock(func, args, force) { * @private */ -Peer.prototype._onConnect = function _onConnect() { - var self = this; - - this.ts = utils.now(); - this.connected = true; - - this.emit('connect'); - - if (this.connectTimeout != null) { - clearTimeout(this.connectTimeout); - this.connectTimeout = null; - } - +Peer.prototype._bip151 = co(function* _bip151() { // Send encinit. Wait for handshake to complete. - if (this.bip151) { - assert(!this.bip151.completed); - this.logger.info('Attempting BIP151 handshake (%s).', this.hostname); - this.send(this.bip151.toEncinit()); - return this.bip151.wait(3000, function(err) { - if (err) - self.error(err, true); - self._onBIP151(); - }); + if (!this.bip151) + return; + + assert(!this.bip151.completed); + + this.logger.info('Attempting BIP151 handshake (%s).', this.hostname); + + this.send(this.bip151.toEncinit()); + + try { + yield this.bip151.wait(3000); + } catch (err) { + this.error(err, true); } - this._onBIP151(); -}; + assert(this.bip151.completed); + + if (this.bip151.handshake) { + this.logger.info('BIP151 handshake complete (%s).', this.hostname); + this.logger.info('Connection is encrypted (%s).', this.hostname); + } +}); /** * Handle post bip151-handshake. * @private */ -Peer.prototype._onBIP151 = function _onBIP151() { - var self = this; +Peer.prototype._bip150 = co(function* _bip150() { + if (!this.bip151 || !this.bip150) + return; - if (this.bip151) { - assert(this.bip151.completed); + assert(!this.bip150.completed); - if (this.bip151.handshake) { - this.logger.info('BIP151 handshake complete (%s).', this.hostname); - this.logger.info('Connection is encrypted (%s).', this.hostname); - } + if (!this.bip151.handshake) + throw new Error('BIP151 handshake was not completed for BIP150.'); - if (this.bip150) { - assert(!this.bip150.completed); + this.logger.info('Attempting BIP150 handshake (%s).', this.hostname); - if (!this.bip151.handshake) - return this.error('BIP151 handshake was not completed for BIP150.'); - - this.logger.info('Attempting BIP150 handshake (%s).', this.hostname); - - if (this.bip150.outbound) { - if (!this.bip150.peerIdentity) - return this.error('No known identity for peer.'); - this.send(this.bip150.toChallenge()); - } - - return this.bip150.wait(3000, function(err) { - if (err) - return self.error(err); - self._onHandshake(); - }); - } + if (this.bip150.outbound) { + if (!this.bip150.peerIdentity) + throw new Error('No known identity for peer.'); + this.send(this.bip150.toChallenge()); } - this._onHandshake(); -}; + yield this.bip150.wait(3000); + + assert(this.bip150.completed); + + if (this.bip150.auth) { + this.logger.info('BIP150 handshake complete (%s).', this.hostname); + this.logger.info('Peer is authed (%s): %s.', + this.hostname, this.bip150.getAddress()); + } +}); /** * Handle post handshake. * @private */ -Peer.prototype._onHandshake = function _onHandshake() { - var self = this; - - if (this.bip150) { - assert(this.bip150.completed); - if (this.bip150.auth) { - this.logger.info('BIP150 handshake complete (%s).', this.hostname); - this.logger.info('Peer is authed (%s): %s.', - this.hostname, this.bip150.getAddress()); - } - } - - this.request('verack', function(err) { - self._onAck(err); - }); - +Peer.prototype._handshake = co(function* _handshake() { // Say hello. this.sendVersion(); @@ -348,32 +407,33 @@ Peer.prototype._onHandshake = function _onHandshake() { && this.pool.server) { this.send(new packets.AddrPacket([this.pool.address])); } -}; -/** - * Handle `ack` event (called on verack). - * @private - */ - -Peer.prototype._onAck = function _onAck(err) { - var self = this; - - if (err) { - this.error(err); - return; - } + yield this.request('verack'); // Wait for _their_ version. if (!this.version) { this.logger.debug( 'Peer sent a verack without a version (%s).', this.hostname); - this.request('version', this._onAck.bind(this)); - return; + + yield this.request('version'); + + assert(this.version); } this.ack = true; + this.logger.debug('Received verack (%s).', this.hostname); +}); + +/** + * Handle `ack` event (called on verack). + * @private + */ + +Peer.prototype._finalize = co(function* _finalize() { + var self = this; + // Setup the ping interval. this.pingTimeout = setInterval(function() { self.sendPing(); @@ -415,52 +475,8 @@ Peer.prototype._onAck = function _onAck(err) { // Start syncing the chain. this.sync(); - this.logger.debug('Received verack (%s).', this.hostname); - - // Finally we can let the pool know - // that this peer is ready to go. - this.emit('ack'); -}; - -/** - * Create the socket and begin connecting. This method - * will use `options.createSocket` if provided. - * @param {String} host - * @param {Number} port - * @returns {net.Socket} - */ - -Peer.prototype.connect = function connect(port, host) { - var self = this; - var socket, proxy, net; - - assert(!this.socket); - - if (this.createSocket) { - socket = this.createSocket(port, host); - } else { - if (utils.isBrowser) { - proxy = require('./proxysocket'); - socket = proxy.connect(this.pool.proxyServer, port, host); - } else { - net = require('net'); - socket = net.connect(port, host); - } - } - - this.logger.debug('Connecting to %s.', this.hostname); - - socket.once('connect', function() { - self.logger.info('Connected to %s.', self.hostname); - }); - - this.connectTimeout = setTimeout(function() { - self.error('Connection timed out.'); - self.ignore(); - }, 10000); - - return socket; -}; + yield co.wait(); +}); /** * Test whether the peer is the loader peer. @@ -476,7 +492,7 @@ Peer.prototype.isLoader = function isLoader() { * @param {Block[]|TX[]|InvItem[]|BroadcastEntry[]} items */ -Peer.prototype.announce = function announce(items) { +Peer.prototype.announce = co(function* announce(items) { var inv = []; var headers = []; var i, item, entry; @@ -495,6 +511,17 @@ Peer.prototype.announce = function announce(items) { if (!this.isWatched(item)) continue; + // Send them the block immediately if + // they're using compact block mode 1. + if (this.compactMode && this.compactMode.mode === 1) { + if (item instanceof Block) { + if (!this.invFilter.added(item.hash())) + continue; + yield this._sendCompactBlock(item, false); + continue; + } + } + // Convert item to block headers // for peers that request it. if (this.preferHeaders && item.toHeaders) { @@ -531,18 +558,18 @@ Peer.prototype.announce = function announce(items) { inv.push(item); } - this.sendInv(inv); + yield this.sendInv(inv); if (headers.length > 0) - this.sendHeaders(headers); -}; + yield this.sendHeaders(headers); +}); /** * Send inv to a peer. * @param {InvItem[]} items */ -Peer.prototype.sendInv = function sendInv(items) { +Peer.prototype.sendInv = co(function* sendInv(items) { var i, chunk; if (this.destroyed) @@ -562,16 +589,16 @@ Peer.prototype.sendInv = function sendInv(items) { for (i = 0; i < items.length; i += 50000) { chunk = items.slice(i, i + 50000); - this.send(new packets.InvPacket(chunk)); + yield this.send(new packets.InvPacket(chunk)); } -}; +}); /** * Send headers to a peer. * @param {Headers[]} items */ -Peer.prototype.sendHeaders = function sendHeaders(items) { +Peer.prototype.sendHeaders = co(function* sendHeaders(items) { var i, chunk; if (this.destroyed) @@ -591,9 +618,9 @@ Peer.prototype.sendHeaders = function sendHeaders(items) { for (i = 0; i < items.length; i += 2000) { chunk = items.slice(i, i + 2000); - this.send(new packets.HeadersPacket(chunk)); + yield this.send(new packets.HeadersPacket(chunk)); } -}; +}); /** * Send a `version` packet. @@ -603,7 +630,7 @@ Peer.prototype.sendVersion = function sendVersion() { var packet = new packets.VersionPacket({ version: constants.VERSION, services: this.pool.services, - ts: bcoin.now(), + ts: time.now(), recv: new NetworkAddress(), from: this.pool.address, nonce: this.pool.localNonce, @@ -611,7 +638,8 @@ Peer.prototype.sendVersion = function sendVersion() { height: this.chain.height, relay: this.options.relay }); - this.send(packet); + + return this.send(packet); }; /** @@ -622,20 +650,18 @@ Peer.prototype.sendPing = function sendPing() { if (!this.version) return; - if (this.version.version <= 60000) { - this.send(new packets.PingPacket()); - return; - } + if (this.version.version <= 60000) + return this.send(new packets.PingPacket()); if (this.challenge) { this.logger.debug('Peer has not responded to ping (%s).', this.hostname); - return; + return Promise.resolve(null); } this.lastPing = utils.ms(); this.challenge = utils.nonce(); - this.send(new packets.PingPacket(this.challenge)); + return this.send(new packets.PingPacket(this.challenge)); }; /** @@ -651,10 +677,10 @@ Peer.prototype.isWatched = function isWatched(item) { if (!item) return true; - if (item instanceof bcoin.tx) + if (item instanceof TX) return item.isWatched(this.spvFilter); - if (item.msg instanceof bcoin.tx) + if (item.msg instanceof TX) return item.msg.isWatched(this.spvFilter); return true; @@ -666,9 +692,9 @@ Peer.prototype.isWatched = function isWatched(item) { Peer.prototype.updateWatch = function updateWatch() { if (!this.options.spv) - return; + return Promise.resolve(null); - this.send(packets.FilterLoadPacket.fromFilter(this.pool.spvFilter)); + return this.send(new packets.FilterLoadPacket(this.pool.spvFilter)); }; /** @@ -677,7 +703,7 @@ Peer.prototype.updateWatch = function updateWatch() { */ Peer.prototype.sendFeeRate = function sendFeeRate(rate) { - this.send(new packets.FeeFilterPacket(rate)); + return this.send(new packets.FeeFilterPacket(rate)); }; /** @@ -685,7 +711,7 @@ Peer.prototype.sendFeeRate = function sendFeeRate(rate) { */ Peer.prototype.destroy = function destroy() { - var i, j, keys, cmd, queue; + var i, j, keys, cmd, queue, hash; if (this.destroyed) return; @@ -722,27 +748,43 @@ Peer.prototype.destroy = function destroy() { queue[j].destroy(); } + keys = Object.keys(this.compactBlocks); + + for (i = 0; i < keys.length; i++) { + hash = keys[i]; + this.compactBlocks[hash].destroy(); + } + this.emit('close'); }; /** * Write data to the peer's socket. * @param {Buffer} data - * @returns {Boolean} + * @returns {Promise} */ Peer.prototype.write = function write(data) { + var self = this; + if (this.destroyed) - return false; + return Promise.resolve(null); this.lastSend = utils.ms(); - return this.socket.write(data); + if (this.socket.write(data) === false) { + return new Promise(function(resolve, reject) { + self.socket.once('drain', resolve); + }); + } + + return Promise.resolve(null); }; /** * Send a packet. * @param {Packet} packet + * @returns {Promise} */ Peer.prototype.send = function send(packet) { @@ -760,7 +802,18 @@ Peer.prototype.send = function send(packet) { } } - this.write(this.framer.packet(packet.cmd, packet.toRaw(), checksum)); + return this.sendRaw(packet.cmd, packet.toRaw(), checksum); +}; + +/** + * Send a packet. + * @param {Packet} packet + * @returns {Promise} + */ + +Peer.prototype.sendRaw = function sendRaw(cmd, body, checksum) { + var payload = this.framer.packet(cmd, body, checksum); + return this.write(payload); }; /** @@ -800,24 +853,25 @@ Peer.prototype.error = function error(err, keep) { * Wait for a packet to be received from peer. * @private * @param {String} cmd - Packet name. - * @param {Function} callback - Returns [Error, Object(payload)]. + * @returns {Promise} - Returns Object(payload). * Executed on timeout or once packet is received. */ -Peer.prototype.request = function request(cmd, callback) { - var entry; +Peer.prototype.request = function request(cmd) { + var self = this; + return new Promise(function(resolve, reject) { + var entry; - if (this.destroyed) - return callback(new Error('Destroyed')); + if (self.destroyed) + return reject(new Error('Destroyed')); - entry = new RequestEntry(this, cmd, callback); + entry = new RequestEntry(self, cmd, resolve, reject); - if (!this.requestMap[cmd]) - this.requestMap[cmd] = []; + if (!self.requestMap[cmd]) + self.requestMap[cmd] = []; - this.requestMap[cmd].push(entry); - - return entry; + self.requestMap[cmd].push(entry); + }); }; /** @@ -839,7 +893,7 @@ Peer.prototype.response = function response(cmd, payload) { if (!entry) return false; - res = entry.callback(null, payload, cmd); + res = entry.resolve(payload); if (res === false) return false; @@ -879,7 +933,7 @@ Peer.prototype.getData = function getData(items) { data[i] = item; } - this.send(new packets.GetDataPacket(data)); + return this.send(new packets.GetDataPacket(data)); }; /** @@ -888,7 +942,34 @@ Peer.prototype.getData = function getData(items) { * @param {Packet} packet */ -Peer.prototype._onPacket = function onPacket(packet) { +Peer.prototype._onPacket = co(function* onPacket(packet) { + var unlock; + + switch (packet.type) { + case packetTypes.VERSION: + case packetTypes.CMPCTBLOCK: + // These can't have locks or stop reads. + return yield this.__onPacket(packet); + default: + unlock = yield this.locker.lock(); + this.socket.pause(); + try { + return yield this.__onPacket(packet); + } finally { + this.socket.resume(); + unlock(); + } + break; + } +}); + +/** + * Handle a packet payload without a lock. + * @private + * @param {Packet} packet + */ + +Peer.prototype.__onPacket = co(function* onPacket(packet) { this.lastRecv = utils.ms(); if (this.bip151 @@ -913,29 +994,29 @@ Peer.prototype._onPacket = function onPacket(packet) { switch (packet.type) { case packetTypes.VERSION: - return this._handleVersion(packet); + return yield this._handleVersion(packet); case packetTypes.VERACK: return this._handleVerack(packet); case packetTypes.PING: - return this._handlePing(packet); + return yield this._handlePing(packet); case packetTypes.PONG: return this._handlePong(packet); case packetTypes.ALERT: return this._handleAlert(packet); case packetTypes.GETADDR: - return this._handleGetAddr(packet); + return yield this._handleGetAddr(packet); case packetTypes.ADDR: return this._handleAddr(packet); case packetTypes.INV: return this._handleInv(packet); case packetTypes.GETDATA: - return this._handleGetData(packet); + return yield this._handleGetData(packet); case packetTypes.NOTFOUND: return this._handleNotFound(packet); case packetTypes.GETBLOCKS: - return this._handleGetBlocks(packet); + return yield this._handleGetBlocks(packet); case packetTypes.GETHEADERS: - return this._handleGetHeaders(packet); + return yield this._handleGetHeaders(packet); case packetTypes.HEADERS: return this._handleHeaders(packet); case packetTypes.SENDHEADERS: @@ -947,7 +1028,7 @@ Peer.prototype._onPacket = function onPacket(packet) { case packetTypes.REJECT: return this._handleReject(packet); case packetTypes.MEMPOOL: - return this._handleMempool(packet); + return yield this._handleMempool(packet); case packetTypes.FILTERLOAD: return this._handleFilterLoad(packet); case packetTypes.FILTERADD: @@ -957,7 +1038,7 @@ Peer.prototype._onPacket = function onPacket(packet) { case packetTypes.MERKLEBLOCK: return this._handleMerkleBlock(packet); case packetTypes.GETUTXOS: - return this._handleGetUTXOs(packet); + return yield this._handleGetUTXOs(packet); case packetTypes.UTXOS: return this._handleUTXOs(packet); case packetTypes.HAVEWITNESS: @@ -967,28 +1048,28 @@ Peer.prototype._onPacket = function onPacket(packet) { case packetTypes.SENDCMPCT: return this._handleSendCmpct(packet); case packetTypes.CMPCTBLOCK: - return this._handleCmpctBlock(packet); + return yield this._handleCmpctBlock(packet); case packetTypes.GETBLOCKTXN: - return this._handleGetBlockTxn(packet); + return yield this._handleGetBlockTxn(packet); case packetTypes.BLOCKTXN: return this._handleBlockTxn(packet); case packetTypes.ENCINIT: - return this._handleEncinit(packet); + return yield this._handleEncinit(packet); case packetTypes.ENCACK: return this._handleEncack(packet); case packetTypes.AUTHCHALLENGE: - return this._handleAuthChallenge(packet); + return yield this._handleAuthChallenge(packet); case packetTypes.AUTHREPLY: - return this._handleAuthReply(packet); + return yield this._handleAuthReply(packet); case packetTypes.AUTHPROPOSE: - return this._handleAuthPropose(packet); + return yield this._handleAuthPropose(packet); case packetTypes.UNKNOWN: return this._handleUnknown(packet); default: assert(false, 'Bad packet type.'); break; } -}; +}); /** * Flush merkle block once all matched @@ -1117,82 +1198,59 @@ Peer.prototype._handleUTXOs = function _handleUTXOs(utxos) { * @private */ -Peer.prototype._handleGetUTXOs = function _handleGetUTXOs(packet) { - var self = this; - var unlock = this._lock(_handleGetUTXOs, [packet, utils.nop]); - var utxos; - - if (!unlock) - return; - - function done(err) { - if (err) { - self.emit('error', err); - return unlock(); - } - unlock(); - } +Peer.prototype._handleGetUTXOs = co(function* _handleGetUTXOs(packet) { + var i, utxos, prevout, hash, index, coin; if (!this.chain.synced) - return done(); + return; if (this.options.selfish) - return done(); + return; if (this.chain.db.options.spv) - return done(); + return; if (packet.prevout.length > 15) - return done(); + return; utxos = new packets.GetUTXOsPacket(); - utils.forEachSerial(packet.prevout, function(prevout, next) { - var hash = prevout.hash; - var index = prevout.index; - var coin; + for (i = 0; i < packet.prevout.length; i++) { + prevout = packet.prevout[i]; + hash = prevout.hash; + index = prevout.index; - if (self.mempool && packet.mempool) { - coin = self.mempool.getCoin(hash, index); + if (this.mempool && packet.mempool) { + coin = this.mempool.getCoin(hash, index); if (coin) { utxos.hits.push(1); utxos.coins.push(coin); - return next(); + continue; } - if (self.mempool.isSpent(hash, index)) { + if (this.mempool.isSpent(hash, index)) { utxos.hits.push(0); - return next(); + continue; } } - self.chain.db.getCoin(hash, index, function(err, coin) { - if (err) - return next(err); + coin = yield this.chain.db.getCoin(hash, index); - if (!coin) { - utxos.hits.push(0); - return next(); - } + if (!coin) { + utxos.hits.push(0); + continue; + } - utxos.hits.push(1); - utxos.coins.push(coin); + utxos.hits.push(1); + utxos.coins.push(coin); + } - next(); - }); - }, function(err) { - if (err) - return done(err); + utxos.height = this.chain.height; + utxos.tip = this.chain.tip.hash; - utxos.height = self.chain.height; - utxos.tip = self.chain.tip.hash; - - self.send(utxos); - - done(); - }); -}; + yield this.send(utxos); +}); /** * Handle `havewitness` packet. @@ -1211,82 +1269,47 @@ Peer.prototype._handleHaveWitness = function _handleHaveWitness(packet) { * @param {GetHeadersPacket} */ -Peer.prototype._handleGetHeaders = function _handleGetHeaders(packet) { - var self = this; +Peer.prototype._handleGetHeaders = co(function* _handleGetHeaders(packet) { var headers = []; - var unlock = this._lock(_handleGetHeaders, [packet, utils.nop]); - - if (!unlock) - return; - - function done(err) { - if (err) { - self.emit('error', err); - return unlock(); - } - self.sendHeaders(headers); - unlock(); - } + var hash, entry; if (!this.chain.synced) - return done(); + return; if (this.options.selfish) - return done(); + return; if (this.chain.db.options.spv) - return done(); + return; if (this.chain.db.options.prune) - return done(); + return; - function collect(err, hash) { - if (err) - return done(err); - - if (!hash) - return done(); - - self.chain.db.get(hash, function(err, entry) { - if (err) - return done(err); - - if (!entry) - return done(); - - (function next(err, entry) { - if (err) - return done(err); - - if (!entry) - return done(); - - headers.push(entry.toHeaders()); - - if (headers.length === 2000) - return done(); - - if (entry.hash === packet.stop) - return done(); - - entry.getNext(next); - })(null, entry); - }); + if (packet.locator.length > 0) { + hash = yield this.chain.findLocator(packet.locator); + if (hash) + hash = yield this.chain.db.getNextHash(hash); + } else { + hash = packet.stop; } - if (packet.locator.length === 0) - return collect(null, packet.stop); + if (hash) + entry = yield this.chain.db.get(hash); - this.chain.findLocator(packet.locator, function(err, hash) { - if (err) - return collect(err); + while (entry) { + headers.push(entry.toHeaders()); - if (!hash) - return collect(); + if (headers.length === 2000) + break; - self.chain.db.getNextHash(hash, collect); - }); -}; + if (entry.hash === packet.stop) + break; + + entry = yield entry.getNext(); + } + + yield this.sendHeaders(headers); +}); /** * Handle `getblocks` packet. @@ -1294,65 +1317,43 @@ Peer.prototype._handleGetHeaders = function _handleGetHeaders(packet) { * @param {GetBlocksPacket} */ -Peer.prototype._handleGetBlocks = function _handleGetBlocks(packet) { - var self = this; +Peer.prototype._handleGetBlocks = co(function* _handleGetBlocks(packet) { var blocks = []; - var unlock = this._lock(_handleGetBlocks, [packet, utils.nop]); - - if (!unlock) - return; - - function done(err) { - if (err) { - self.emit('error', err); - return unlock(); - } - self.sendInv(blocks); - unlock(); - } + var hash; if (!this.chain.synced) - return done(); + return; if (this.options.selfish) - return done(); + return; if (this.chain.db.options.spv) - return done(); + return; if (this.chain.db.options.prune) - return done(); + return; - this.chain.findLocator(packet.locator, function(err, tip) { - if (err) - return done(err); + hash = yield this.chain.findLocator(packet.locator); - if (!tip) - return done(); + if (hash) + hash = yield this.chain.db.getNextHash(hash); - (function next(hash) { - self.chain.db.getNextHash(hash, function(err, hash) { - if (err) - return done(err); + while (hash) { + blocks.push(new InvItem(constants.inv.BLOCK, hash)); - if (!hash) - return done(); + if (hash === packet.stop) + break; - blocks.push(new InvItem(constants.inv.BLOCK, hash)); + if (blocks.length === 500) { + this.hashContinue = hash; + break; + } - if (hash === packet.stop) - return done(); + hash = yield this.chain.db.getNextHash(hash); + } - if (blocks.length === 500) { - self.hashContinue = hash; - return done(); - } - - next(hash); - }); - })(tip); - }); -}; + yield this.sendInv(blocks); +}); /** * Handle `version` packet. @@ -1360,85 +1361,70 @@ Peer.prototype._handleGetBlocks = function _handleGetBlocks(packet) { * @param {VersionPacket} */ -Peer.prototype._handleVersion = function _handleVersion(version) { - var self = this; +Peer.prototype._handleVersion = co(function* _handleVersion(version) { + if (this.version) + throw new Error('Peer sent a duplicate version.'); if (!this.network.selfConnect) { if (version.nonce.cmp(this.pool.localNonce) === 0) { - this.error('We connected to ourself. Oops.'); this.ignore(); - return; + throw new Error('We connected to ourself. Oops.'); } } if (version.version < constants.MIN_VERSION) { - this.error('Peer does not support required protocol version.'); this.ignore(); - return; + throw new Error('Peer does not support required protocol version.'); } if (this.outbound) { if (!version.hasNetwork()) { - this.error('Peer does not support network services.'); this.ignore(); - return; + throw new Error('Peer does not support network services.'); } } if (this.options.headers) { if (!version.hasHeaders()) { - this.error('Peer does not support getheaders.'); this.ignore(); - return; + throw new Error('Peer does not support getheaders.'); } } if (this.options.spv) { if (!version.hasBloom()) { - this.error('Peer does not support BIP37.'); this.ignore(); - return; + throw new Error('Peer does not support BIP37.'); } } if (this.options.witness) { - if (!version.hasWitness()) { + this.haveWitness = version.hasWitness(); + + if (!this.haveWitness) { if (!this.network.oldWitness) { - this.error('Peer does not support segregated witness.'); this.ignore(); - return; + throw new Error('Peer does not support segregated witness.'); } - return this.request('havewitness', function(err) { - if (err) { - self.error('Peer does not support segregated witness.'); - self.ignore(); - return; - } - self._finishVersion(version); - }); + + try { + yield this.request('havewitness'); + } catch (err) { + this.ignore(); + throw new Error('Peer does not support segregated witness.'); + } + + this.haveWitness = true; } } - this._finishVersion(version); -}; - -/** - * Finish handling `version` packet. - * @private - * @param {VersionPacket} - */ - -Peer.prototype._finishVersion = function _finishVersion(version) { - if (version.hasWitness()) - this.haveWitness = true; - - if (!version.relay) - this.relay = false; - - this.send(new packets.VerackPacket()); + this.relay = version.relay; this.version = version; + this.fire('version', version); -}; + + yield this.send(new packets.VerackPacket()); +}); /** * Handle `verack` packet. @@ -1457,89 +1443,154 @@ Peer.prototype._handleVerack = function _handleVerack(packet) { */ Peer.prototype._handleMempool = function _handleMempool(packet) { - var self = this; var items = []; var i, hashes; - var unlock = this._lock(_handleMempool, [packet, utils.nop]); - - if (!unlock) - return; - - function done(err) { - if (err) { - self.emit('error', err); - return unlock(); - } - unlock(); - } if (!this.mempool) - return done(); + return; if (!this.chain.synced) - return done(); + return; if (this.options.selfish) - return done(); + return; hashes = this.mempool.getSnapshot(); for (i = 0; i < hashes.length; i++) items.push(new InvItem(constants.inv.TX, hashes[i])); - self.logger.debug('Sending mempool snapshot (%s).', self.hostname); + this.logger.debug('Sending mempool snapshot (%s).', this.hostname); - self.sendInv(items); + return this.sendInv(items); +}; + +/** + * Get a block/tx from the broadcast map. + * @param {InvItem} item + * @returns {Promise} + */ + +Peer.prototype._getBroadcasted = function _getBroadcasted(item) { + var entry = this.pool.invMap[item.hash]; + + if (!entry) + return; + + this.logger.debug( + 'Peer requested %s %s as a %s packet (%s).', + entry.type === constants.inv.TX ? 'tx' : 'block', + utils.revHex(entry.hash), + item.hasWitness() ? 'witness' : 'normal', + this.hostname); + + entry.ack(this); + + if (!entry.msg) + return; + + if (item.isTX()) { + if (entry.type !== constants.inv.TX) + return; + } else { + if (entry.type !== constants.inv.BLOCK) + return; + } + + return entry.msg; }; /** * Get a block/tx either from the broadcast map, mempool, or blockchain. * @param {InvItem} item - * @param {Function} callback - Returns - * [Error, {@link Block}|{@link MempoolEntry}]. + * @returns {Promise} */ -Peer.prototype._getItem = function _getItem(item, callback) { - var entry = this.pool.invMap[item.hash]; +Peer.prototype._getItem = co(function* _getItem(item) { + var entry = this._getBroadcasted(item); - if (entry) { - this.logger.debug( - 'Peer requested %s %s as a %s packet (%s).', - entry.type === constants.inv.TX ? 'tx' : 'block', - utils.revHex(entry.hash), - item.hasWitness() ? 'witness' : 'normal', - this.hostname); - - entry.ack(this); - - if (entry.msg) { - if (item.isTX()) { - if (entry.type === constants.inv.TX) - return callback(null, entry.msg); - } else { - if (entry.type === constants.inv.BLOCK) - return callback(null, entry.msg); - } - return callback(); - } - } + if (entry) + return entry; if (this.options.selfish) - return callback(); + return; if (item.isTX()) { if (!this.mempool) - return callback(); - return callback(null, this.mempool.getTX(item.hash)); + return; + return this.mempool.getTX(item.hash); } if (this.chain.db.options.spv) - return callback(); + return; if (this.chain.db.options.prune) - return callback(); + return; - this.chain.db.getBlock(item.hash, callback); + return yield this.chain.db.getBlock(item.hash); +}); + +/** + * Send a block from the broadcast list or chain. + * @param {InvItem} item + * @returns {Boolean} + */ + +Peer.prototype._sendBlock = co(function* _sendBlock(item) { + var block = this._getBroadcasted(item); + + // Check for a broadcasted item first. + if (block) { + yield this.send(new packets.BlockPacket(block, item.hasWitness())); + return true; + } + + if (this.options.selfish + || this.chain.db.options.spv + || this.chain.db.options.prune) { + return false; + } + + // If we have the same serialization, we + // can write the raw binary to the socket. + if (item.hasWitness() === !!this.options.witness) { + block = yield this.chain.db.getRawBlock(item.hash); + + if (!block) + return false; + + yield this.sendRaw('block', block); + } else { + block = yield this.chain.db.getBlock(item.hash); + + if (!block) + return false; + + yield this.send(new packets.BlockPacket(block, item.hasWitness())); + } + + return true; +}); + +/** + * Send a compact block. + * @param {InvItem} item + * @returns {Boolean} + */ + +Peer.prototype._sendCompactBlock = function _sendCompactBlock(block, witness) { + // Try again with a new nonce + // if we get a siphash collision. + for (;;) { + try { + block = BIP152.CompactBlock.fromBlock(block); + } catch (e) { + continue; + } + break; + } + + return this.send(new packets.CmpctBlockPacket(block, witness)); }; /** @@ -1548,134 +1599,118 @@ Peer.prototype._getItem = function _getItem(item, callback) { * @param {GetDataPacket} */ -Peer.prototype._handleGetData = function _handleGetData(packet) { - var self = this; +Peer.prototype._handleGetData = co(function* _handleGetData(packet) { var notFound = []; var items = packet.items; - var unlock = this._lock(_handleGetData, [packet, utils.nop]); + var i, j, item, tx, block, result; - if (!unlock) - return; + if (items.length > 50000) + throw new Error('getdata size too large (' + items.length + ').'); - function done(err) { - if (err) { - self.emit('error', err); - return unlock(); - } - unlock(); - } + for (i = 0; i < items.length; i++) { + item = items[i]; - if (items.length > 50000) { - this.error('getdata size too large (%s).', items.length); - return done(); - } + if (item.isTX()) { + tx = yield this._getItem(item); - utils.forEachSerial(items, function(item, next) { - var i, tx, block; - - self._getItem(item, function(err, entry) { - if (err) - return next(err); - - if (!entry) { + if (!tx) { notFound.push(item); - return next(); + continue; } - if (item.isTX()) { - tx = entry; + // Coinbases are an insta-ban from any node. + // This should technically never happen, but + // it's worth keeping here just in case. A + // 24-hour ban from any node is rough. + if (tx.isCoinbase()) { + notFound.push(item); + this.logger.warning('Failsafe: tried to relay a coinbase.'); + continue; + } - // Coinbases are an insta-ban from any node. - // This should technically never happen, but - // it's worth keeping here just in case. A - // 24-hour ban from any node is rough. - if (tx.isCoinbase()) { + yield this.send(new packets.TXPacket(tx, item.hasWitness())); + + continue; + } + + switch (item.type) { + case constants.inv.BLOCK: + case constants.inv.WITNESS_BLOCK: + result = yield this._sendBlock(item); + if (!result) { notFound.push(item); - self.logger.warning('Failsafe: tried to relay a coinbase.'); - return next(); + continue; + } + break; + case constants.inv.FILTERED_BLOCK: + case constants.inv.WITNESS_FILTERED_BLOCK: + if (!this.spvFilter) { + notFound.push(item); + continue; } - self.send(new packets.TXPacket(tx, item.hasWitness())); + block = yield this._getItem(item); - return next(); - } - - block = entry; - - switch (item.type) { - case constants.inv.BLOCK: - case constants.inv.WITNESS_BLOCK: - self.send(new packets.BlockPacket(block, item.hasWitness())); - break; - case constants.inv.FILTERED_BLOCK: - case constants.inv.WITNESS_FILTERED_BLOCK: - if (!self.spvFilter) { - notFound.push(item); - return next(); - } - - block = block.toMerkle(self.spvFilter); - - self.send(new packets.MerkleBlockPacket(block)); - - for (i = 0; i < block.txs.length; i++) { - tx = block.txs[i]; - self.send(new packets.TXPacket(tx, item.hasWitness())); - } - - break; - case constants.inv.CMPCT_BLOCK: - // Fallback to full block. - if (block.height < self.chain.tip.height - 10) { - self.send(new packets.BlockPacket(block, false)); - break; - } - - // Try again with a new nonce - // if we get a siphash collision. - for (;;) { - try { - block = bcoin.bip152.CompactBlock.fromBlock(block); - } catch (e) { - continue; - } - break; - } - - self.send(new packets.CmpctBlockPacket(block, false)); - break; - default: - self.logger.warning( - 'Peer sent an unknown getdata type: %s (%s).', - item.type, - self.hostname); + if (!block) { notFound.push(item); - return next(); - } + continue; + } - if (item.hash === self.hashContinue) { - self.sendInv(new InvItem(constants.inv.BLOCK, self.chain.tip.hash)); - self.hashContinue = null; - } + block = block.toMerkle(this.spvFilter); - next(); - }); - }, function(err) { - if (err) - return done(err); + yield this.send(new packets.MerkleBlockPacket(block)); - self.logger.debug( - 'Served %d items with getdata (notfound=%d) (%s).', - items.length - notFound.length, - notFound.length, - self.hostname); + for (j = 0; j < block.txs.length; j++) { + tx = block.txs[j]; + yield this.send(new packets.TXPacket(tx, item.hasWitness())); + } - if (notFound.length > 0) - self.send(new packets.NotFoundPacket(notFound)); + break; + case constants.inv.CMPCT_BLOCK: + // Fallback to full block. + if (block.height < this.chain.tip.height - 10) { + result = yield this._sendBlock(item); + if (!result) { + notFound.push(item); + continue; + } + break; + } - done(); - }); -}; + block = yield this._getItem(item); + + if (!block) { + notFound.push(item); + continue; + } + + yield this._sendCompactBlock(block, false); + + break; + default: + this.logger.warning( + 'Peer sent an unknown getdata type: %s (%s).', + item.type, + this.hostname); + notFound.push(item); + continue; + } + + if (item.hash === this.hashContinue) { + yield this.sendInv(new InvItem(constants.inv.BLOCK, this.chain.tip.hash)); + this.hashContinue = null; + } + } + + this.logger.debug( + 'Served %d items with getdata (notfound=%d) (%s).', + items.length - notFound.length, + notFound.length, + this.hostname); + + if (notFound.length > 0) + yield this.send(new packets.NotFoundPacket(notFound)); +}); /** * Handle `notfound` packet. @@ -1716,11 +1751,11 @@ Peer.prototype._handleAddr = function _handleAddr(packet) { * @param {PingPacket} */ -Peer.prototype._handlePing = function _handlePing(packet) { - if (packet.nonce) - this.send(new packets.PongPacket(packet.nonce)); +Peer.prototype._handlePing = co(function* _handlePing(packet) { this.fire('ping', this.minPing); -}; + if (packet.nonce) + yield this.send(new packets.PongPacket(packet.nonce)); +}); /** * Handle `pong` packet. @@ -1767,7 +1802,7 @@ Peer.prototype._handlePong = function _handlePong(packet) { * @param {GetAddrPacket} */ -Peer.prototype._handleGetAddr = function _handleGetAddr(packet) { +Peer.prototype._handleGetAddr = co(function* _handleGetAddr(packet) { var items = []; var i, addr; @@ -1804,8 +1839,8 @@ Peer.prototype._handleGetAddr = function _handleGetAddr(packet) { items.length, this.hostname); - this.send(new packets.AddrPacket(items)); -}; + yield this.send(new packets.AddrPacket(items)); +}); /** * Handle `inv` packet. @@ -1955,21 +1990,16 @@ Peer.prototype._handleAlert = function _handleAlert(alert) { * @param {EncinitPacket} */ -Peer.prototype._handleEncinit = function _handleEncinit(packet) { +Peer.prototype._handleEncinit = co(function* _handleEncinit(packet) { if (!this.bip151) return; - try { - this.bip151.encinit(packet.publicKey, packet.cipher); - } catch (e) { - this.error(e); - return; - } - - this.send(this.bip151.toEncack()); + this.bip151.encinit(packet.publicKey, packet.cipher); this.fire('encinit', packet); -}; + + yield this.send(this.bip151.toEncack()); +}); /** * Handle `encack` packet. @@ -1981,12 +2011,7 @@ Peer.prototype._handleEncack = function _handleEncack(packet) { if (!this.bip151) return; - try { - this.bip151.encack(packet.publicKey); - } catch (e) { - this.error(e); - return; - } + this.bip151.encack(packet.publicKey); this.fire('encack', packet); }; @@ -1997,23 +2022,18 @@ Peer.prototype._handleEncack = function _handleEncack(packet) { * @param {AuthChallengePacket} */ -Peer.prototype._handleAuthChallenge = function _handleAuthChallenge(packet) { +Peer.prototype._handleAuthChallenge = co(function* _handleAuthChallenge(packet) { var sig; if (!this.bip150) return; - try { - sig = this.bip150.challenge(packet.hash); - } catch (e) { - this.error(e); - return; - } - - this.send(new packets.AuthReplyPacket(sig)); + sig = this.bip150.challenge(packet.hash); this.fire('authchallenge', packet.hash); -}; + + yield this.send(new packets.AuthReplyPacket(sig)); +}); /** * Handle `authreply` packet. @@ -2021,24 +2041,19 @@ Peer.prototype._handleAuthChallenge = function _handleAuthChallenge(packet) { * @param {AuthReplyPacket} */ -Peer.prototype._handleAuthReply = function _handleAuthReply(packet) { +Peer.prototype._handleAuthReply = co(function* _handleAuthReply(packet) { var hash; if (!this.bip150) return; - try { - hash = this.bip150.reply(packet.signature); - } catch (e) { - this.error(e); - return; - } + hash = this.bip150.reply(packet.signature); if (hash) - this.send(new packets.AuthProposePacket(hash)); + yield this.send(new packets.AuthProposePacket(hash)); this.fire('authreply', packet.signature); -}; +}); /** * Handle `authpropose` packet. @@ -2046,23 +2061,18 @@ Peer.prototype._handleAuthReply = function _handleAuthReply(packet) { * @param {AuthProposePacket} */ -Peer.prototype._handleAuthPropose = function _handleAuthPropose(packet) { +Peer.prototype._handleAuthPropose = co(function* _handleAuthPropose(packet) { var hash; if (!this.bip150) return; - try { - hash = this.bip150.propose(packet.hash); - } catch (e) { - this.error(e); - return; - } + hash = this.bip150.propose(packet.hash); - this.send(new packets.AuthChallengePacket(hash)); + yield this.send(new packets.AuthChallengePacket(hash)); this.fire('authpropose', packet.hash); -}; +}); /** * Handle an unknown packet. @@ -2089,8 +2099,7 @@ Peer.prototype._handleSendCmpct = function _handleSendCmpct(packet) { return; } - if (packet.mode !== 0) { - // Ignore (we can't do mode 1 yet). + if (packet.mode > 1) { this.logger.info('Peer request compact blocks mode %d (%s).', packet.mode, this.hostname); return; @@ -2108,8 +2117,7 @@ Peer.prototype._handleSendCmpct = function _handleSendCmpct(packet) { * @param {CmpctBlockPacket} */ -Peer.prototype._handleCmpctBlock = function _handleCmpctBlock(packet) { - var self = this; +Peer.prototype._handleCmpctBlock = co(function* _handleCmpctBlock(packet) { var block = packet.block; var hash = block.hash('hex'); var result; @@ -2131,7 +2139,6 @@ Peer.prototype._handleCmpctBlock = function _handleCmpctBlock(packet) { return; } - // Sort of a lock too. this.compactBlocks[hash] = block; result = block.fillMempool(this.mempool); @@ -2145,19 +2152,22 @@ Peer.prototype._handleCmpctBlock = function _handleCmpctBlock(packet) { return; } - this.send(new packets.GetBlockTxnPacket(block.toRequest())); + yield this.send(new packets.GetBlockTxnPacket(block.toRequest())); this.logger.debug( 'Received semi-full compact block %s (%s).', block.rhash, this.hostname); - block.startTimeout(10000, function() { - self.logger.debug( + try { + yield block.wait(10000); + } catch (e) { + this.logger.debug( 'Compact block timed out: %s (%s).', - block.rhash, self.hostname); - delete self.compactBlocks[hash]; - }); -}; + block.rhash, this.hostname); + + delete this.compactBlocks[hash]; + } +}); /** * Handle `getblocktxn` packet. @@ -2165,56 +2175,44 @@ Peer.prototype._handleCmpctBlock = function _handleCmpctBlock(packet) { * @param {GetBlockTxnPacket} */ -Peer.prototype._handleGetBlockTxn = function _handleGetBlockTxn(packet) { - var self = this; +Peer.prototype._handleGetBlockTxn = co(function* _handleGetBlockTxn(packet) { var req = packet.request; - var res, item; - - function done(err) { - if (err) { - self.emit('error', err); - return; - } - self.fire('blocktxn', req); - } + var res, item, block; if (this.chain.db.options.spv) - return done(); + return; if (this.chain.db.options.prune) - return done(); + return; if (this.options.selfish) - return done(); + return; item = new InvItem(constants.inv.BLOCK, req.hash); - this._getItem(item, function(err, block) { - if (err) - return done(err); + block = yield this._getItem(item); - if (!block) { - self.logger.debug( - 'Peer sent getblocktxn for non-existent block (%s).', - self.hostname); - self.setMisbehavior(100); - return done(); - } + if (!block) { + this.logger.debug( + 'Peer sent getblocktxn for non-existent block (%s).', + this.hostname); + this.setMisbehavior(100); + return; + } - if (block.height < self.chain.tip.height - 15) { - self.logger.debug( - 'Peer sent a getblocktxn for a block > 15 deep (%s)', - self.hostname); - return done(); - } + if (block.height < this.chain.tip.height - 15) { + this.logger.debug( + 'Peer sent a getblocktxn for a block > 15 deep (%s)', + this.hostname); + return; + } - res = bcoin.bip152.TXResponse.fromBlock(block, req); + res = BIP152.TXResponse.fromBlock(block, req); - self.send(new packets.BlockTxnPacket(res, false)); + yield this.send(new packets.BlockTxnPacket(res, false)); - done(); - }); -}; + this.fire('blocktxn', req); +}); /** * Handle `blocktxn` packet. @@ -2231,9 +2229,8 @@ Peer.prototype._handleBlockTxn = function _handleBlockTxn(packet) { return; } - this.fire('getblocktxn', res); + block.complete(); - block.stopTimeout(); delete this.compactBlocks[res.hash]; if (!block.fillMissing(res)) { @@ -2247,6 +2244,7 @@ Peer.prototype._handleBlockTxn = function _handleBlockTxn(packet) { block.rhash, this.hostname); this.fire('block', block.toBlock()); + this.fire('getblocktxn', res); }; /** @@ -2256,9 +2254,9 @@ Peer.prototype._handleBlockTxn = function _handleBlockTxn(packet) { Peer.prototype.sendAlert = function sendAlert(alert) { if (!this.invFilter.added(alert.hash())) - return; + return Promise.resolve(null); - this.send(alert); + return this.send(alert); }; /** @@ -2287,7 +2285,7 @@ Peer.prototype.sendGetHeaders = function sendGetHeaders(locator, stop) { this.logger.debug('Height: %d, Hash: %s, Stop: %s', height, hash, stop); - this.send(packet); + return this.send(packet); }; /** @@ -2315,7 +2313,7 @@ Peer.prototype.sendGetBlocks = function getBlocks(locator, stop) { this.logger.debug('Height: %d, Hash: %s, Stop: %s', height, hash, stop); - this.send(packet); + return this.send(packet); }; /** @@ -2324,20 +2322,20 @@ Peer.prototype.sendGetBlocks = function getBlocks(locator, stop) { Peer.prototype.sendMempool = function sendMempool() { if (!this.version) - return; + return Promise.resolve(null); if (!this.version.hasBloom()) { this.logger.debug( 'Cannot request mempool for non-bloom peer (%s).', this.hostname); - return; + return Promise.resolve(null); } this.logger.debug( 'Requesting inv packet from peer with mempool (%s).', this.hostname); - this.send(new packets.MempoolPacket()); + return this.send(new packets.MempoolPacket()); }; /** @@ -2362,7 +2360,7 @@ Peer.prototype.sendReject = function sendReject(code, reason, obj) { 'Sending reject packet to peer (%s).', this.hostname); - this.send(reject); + return this.send(reject); }; /** @@ -2370,9 +2368,8 @@ Peer.prototype.sendReject = function sendReject(code, reason, obj) { */ Peer.prototype.sendCompact = function sendCompact() { - var cmpct = new packets.SendCmpctPacket(0, 1); this.logger.info('Initializing compact blocks (%s).', this.hostname); - this.send(cmpct); + return this.send(new packets.SendCmpctPacket(0, 1)); }; /** @@ -2420,9 +2417,10 @@ Peer.prototype.ignore = function ignore() { */ Peer.prototype.reject = function reject(obj, code, reason, score) { - this.sendReject(code, reason, obj); + var promise = this.sendReject(code, reason, obj); if (score > 0) this.setMisbehavior(score); + return promise; }; /** @@ -2430,102 +2428,73 @@ Peer.prototype.reject = function reject(obj, code, reason, score) { * locator and resolving orphan root. * @param {Hash} tip - Tip to build chain locator from. * @param {Hash} orphan - Orphan hash to resolve. - * @param {Function} callback + * @returns {Promise} */ -Peer.prototype.resolveOrphan = function resolveOrphan(tip, orphan, callback) { - var self = this; - var root; - - callback = utils.ensure(callback); +Peer.prototype.resolveOrphan = co(function* resolveOrphan(tip, orphan) { + var root, locator; assert(orphan); - this.chain.getLocator(tip, function(err, locator) { - if (err) - return callback(err); + locator = yield this.chain.getLocator(tip); + root = this.chain.getOrphanRoot(orphan); - root = self.chain.getOrphanRoot(orphan); + // Was probably resolved. + if (!root) { + this.logger.debug('Orphan root was already resolved.'); + return; + } - // Was probably resolved. - if (!root) { - self.logger.debug('Orphan root was already resolved.'); - return callback(); - } - - self.sendGetBlocks(locator, root); - - callback(); - }); -}; + yield this.sendGetBlocks(locator, root); +}); /** * Send `getheaders` to peer after building locator. * @param {Hash} tip - Tip to build chain locator from. * @param {Hash?} stop - * @param {Function} callback + * @returns {Promise} */ -Peer.prototype.getHeaders = function getHeaders(tip, stop, callback) { - var self = this; - - callback = utils.ensure(callback); - - this.chain.getLocator(tip, function(err, locator) { - if (err) - return callback(err); - - self.sendGetHeaders(locator, stop); - - callback(); - }); -}; +Peer.prototype.getHeaders = co(function* getHeaders(tip, stop) { + var locator = yield this.chain.getLocator(tip); + return this.sendGetHeaders(locator, stop); +}); /** * Send `getblocks` to peer after building locator. * @param {Hash} tip - Tip hash to build chain locator from. * @param {Hash?} stop - * @param {Function} callback + * @returns {Promise} */ -Peer.prototype.getBlocks = function getBlocks(tip, stop, callback) { - var self = this; - - callback = utils.ensure(callback); - - this.chain.getLocator(tip, function(err, locator) { - if (err) - return callback(err); - - self.sendGetBlocks(locator, stop); - - callback(); - }); -}; +Peer.prototype.getBlocks = co(function* getBlocks(tip, stop) { + var locator = yield this.chain.getLocator(tip); + return this.sendGetBlocks(locator, stop); +}); /** * Start syncing from peer. - * @param {Function} callback + * @returns {Promise} */ -Peer.prototype.sync = function sync(callback) { +Peer.prototype.sync = function sync() { var tip; if (!this.pool.syncing) - return; + return Promise.resolve(null); if (!this.ack) - return; + return Promise.resolve(null); if (this.syncSent) - return; + return Promise.resolve(null); if (!this.version.hasNetwork()) - return; + return Promise.resolve(null); if (!this.isLoader()) { if (!this.chain.synced) - return; + return Promise.resolve(null); } // Ask for the mempool if we're synced. @@ -2540,10 +2509,10 @@ Peer.prototype.sync = function sync(callback) { if (!this.chain.tip.isGenesis()) tip = this.chain.tip.prevBlock; - return this.getHeaders(tip, null, callback); + return this.getHeaders(tip); } - this.getBlocks(null, null, callback); + return this.getBlocks(); }; /** @@ -2566,10 +2535,11 @@ Peer.prototype.inspect = function inspect() { * @constructor */ -function RequestEntry(peer, cmd, callback) { +function RequestEntry(peer, cmd, resolve, reject) { this.peer = peer; this.cmd = cmd; - this.callback = callback; + this.resolve = resolve; + this.reject = reject; this.id = peer.uid++; this.onTimeout = this._onTimeout.bind(this); this.timeout = setTimeout(this.onTimeout, this.peer.requestTimeout); @@ -2584,7 +2554,7 @@ RequestEntry.prototype._onTimeout = function _onTimeout() { if (utils.binaryRemove(queue, this, compare)) { if (queue.length === 0) delete this.peer.requestMap[this.cmd]; - this.callback(new Error('Timed out: ' + this.cmd)); + this.reject(new Error('Timed out: ' + this.cmd)); } }; diff --git a/lib/net/pool.js b/lib/net/pool.js index b95939f2..f8b49224 100644 --- a/lib/net/pool.js +++ b/lib/net/pool.js @@ -7,17 +7,26 @@ 'use strict'; -var bcoin = require('../env'); var AsyncObject = require('../utils/async'); var EventEmitter = require('events').EventEmitter; var utils = require('../utils/utils'); var IP = require('../utils/ip'); -var assert = utils.assert; -var constants = bcoin.constants; -var VerifyError = bcoin.errors.VerifyError; +var co = require('../utils/co'); +var assert = require('assert'); +var constants = require('../protocol/constants'); +var VerifyError = require('../utils/errors').VerifyError; var NetworkAddress = require('../primitives/netaddress'); -var InvItem = bcoin.invitem; var VerifyResult = utils.VerifyResult; +var Address = require('../primitives/address'); +var BIP150 = require('./bip150'); +var Bloom = require('../utils/bloom'); +var ec = require('../crypto/ec'); +var InvItem = require('../primitives/invitem'); +var Locker = require('../utils/locker'); +var Network = require('../protocol/network'); +var time = require('./timedata'); +var Peer = require('./peer'); +var TX = require('../primitives/tx'); /** * A pool of peers for handling all network activity. @@ -101,7 +110,7 @@ function Pool(options) { this.connected = false; this.uid = 0; this.createServer = null; - this.locker = new bcoin.locker(this); + this.locker = new Locker(); this.proxyServer = null; this.auth = null; this.identityKey = null; @@ -181,7 +190,7 @@ Pool.prototype._initOptions = function _initOptions() { if (this.options.bip150) { this.options.bip151 = true; - this.auth = new bcoin.bip150.AuthDB(); + this.auth = new BIP150.AuthDB(); if (this.options.authPeers) this.auth.setAuthorized(this.options.authPeers); @@ -189,10 +198,10 @@ Pool.prototype._initOptions = function _initOptions() { if (this.options.knownPeers) this.auth.setKnown(this.options.knownPeers); - this.identityKey = this.options.identityKey || bcoin.ec.generatePrivateKey(); + this.identityKey = this.options.identityKey || ec.generatePrivateKey(); assert(Buffer.isBuffer(this.identityKey), 'Identity key must be a buffer.'); - assert(bcoin.ec.privateKeyVerify(this.identityKey), + assert(ec.privateKeyVerify(this.identityKey), 'Invalid identity key.'); } @@ -221,10 +230,10 @@ Pool.prototype._initOptions = function _initOptions() { } if (this.options.spv) - this.spvFilter = bcoin.bloom.fromRate(10000, 0.001, constants.bloom.NONE); + this.spvFilter = Bloom.fromRate(10000, 0.001, constants.bloom.NONE); if (!this.options.mempool) - this.txFilter = new bcoin.bloom.rolling(50000, 0.000001); + this.txFilter = new Bloom.Rolling(50000, 0.000001); if (this.options.requestTimeout != null) this.requestTimeout = this.options.requestTimeout; @@ -274,68 +283,52 @@ Pool.prototype._init = function _init() { }); }; -/** - * Invoke mutex lock. - * @private - */ - -Pool.prototype._lock = function _lock(func, args, force) { - return this.locker.lock(func, args, force); -}; - /** * Open the pool, wait for the chain to load. * @alias Pool#open - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype._open = function _open(callback) { - var self = this; - var key; +Pool.prototype._open = co(function* _open() { + var ip, key; - this.getIP(function(err, ip) { - if (err) - self.logger.error(err); + try { + ip = yield this.getIP(); + } catch (e) { + this.logger.error(e); + } - if (ip) { - self.address.setHost(ip); - self.logger.info('External IP found: %s.', ip); - } + if (ip) { + this.address.setHost(ip); + this.logger.info('External IP found: %s.', ip); + } - function open(callback) { - if (self.mempool) - self.mempool.open(callback); - else - self.chain.open(callback); - } + if (this.mempool) + yield this.mempool.open(); + else + yield this.chain.open(); - open(function(err) { - if (err) - return callback(err); + this.logger.info('Pool loaded (maxpeers=%d).', this.maxPeers); - self.logger.info('Pool loaded (maxpeers=%d).', self.maxPeers); + if (this.identityKey) { + key = ec.publicKeyCreate(this.identityKey, true); + this.logger.info('Identity public key: %s.', key.toString('hex')); + this.logger.info('Identity address: %s.', BIP150.address(key)); + } - if (self.identityKey) { - key = bcoin.ec.publicKeyCreate(self.identityKey, true); - self.logger.info('Identity public key: %s.', key.toString('hex')); - self.logger.info('Identity address: %s.', bcoin.bip150.address(key)); - } + if (!this.options.listen) + return; - if (!self.options.listen) - return callback(); - - self.listen(callback); - }); - }); -}; + yield this.listen(); +}); /** * Close and destroy the pool. * @alias Pool#close - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype._close = function close(callback) { +Pool.prototype._close = co(function* close() { var i, items, hashes, hash; this.stopSync(); @@ -362,8 +355,8 @@ Pool.prototype._close = function close(callback) { this.pendingWatch = null; } - this.unlisten(callback); -}; + yield this.unlisten(); +}); /** * Connect to the network. @@ -406,22 +399,20 @@ Pool.prototype.connect = function connect() { /** * Start listening on a server socket. - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype.listen = function listen(callback) { +Pool.prototype.listen = function listen() { var self = this; var net; - callback = utils.ensure(callback); - assert(!this.server, 'Server already listening.'); if (this.createServer) { this.server = this.createServer(); } else { if (utils.isBrowser) - return utils.nextTick(callback); + return; net = require('net'); this.server = new net.Server(); } @@ -437,25 +428,29 @@ Pool.prototype.listen = function listen(callback) { data.address, data.port); }); - this.server.listen(this.port, '0.0.0.0', callback); + return new Promise(function(resolve, reject) { + self.server.listen(self.port, '0.0.0.0', co.wrap(resolve, reject)); + }); }; /** * Stop listening on server socket. - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype.unlisten = function unlisten(callback) { - callback = utils.ensure(callback); +Pool.prototype.unlisten = function unlisten() { + var self = this; if (utils.isBrowser) - return utils.nextTick(callback); + return; if (!this.server) - return utils.nextTick(callback); + return; - this.server.close(callback); - this.server = null; + return new Promise(function(resolve, reject) { + self.server.close(co.wrap(resolve, reject)); + self.server = null; + }); }; /** @@ -721,20 +716,32 @@ Pool.prototype.stopSync = function stopSync() { * @private * @param {Headers[]} headers * @param {Peer} peer - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype._handleHeaders = function _handleHeaders(headers, peer, callback) { - var self = this; - var ret, last; +Pool.prototype._handleHeaders = co(function* _handleHeaders(headers, peer) { + var unlock = yield this.locker.lock(); + try { + return yield this.__handleHeaders(headers, peer); + } finally { + unlock(); + } +}); - callback = this._lock(_handleHeaders, [headers, peer, callback]); +/** + * Handle `headers` packet from + * a given peer without a lock. + * @private + * @param {Headers[]} headers + * @param {Peer} peer + * @returns {Promise} + */ - if (!callback) - return; +Pool.prototype.__handleHeaders = co(function* _handleHeaders(headers, peer) { + var i, ret, header, hash, last; if (!this.options.headers) - return callback(); + return; ret = new VerifyResult(); @@ -752,53 +759,49 @@ Pool.prototype._handleHeaders = function _handleHeaders(headers, peer, callback) this.startTimeout(); } - utils.forEachSerial(headers, function(header, next) { - var hash = header.hash('hex'); + for (i = 0; i < headers.length; i++) { + header = headers[i]; + hash = header.hash('hex'); if (last && header.prevBlock !== last) { peer.setMisbehavior(100); - return next(new Error('Bad header chain.')); + throw new Error('Bad header chain.'); } if (!header.verify(ret)) { peer.reject(header, 'invalid', ret.reason, 100); - return next(new Error('Invalid header.')); + throw new Error('Invalid header.'); } last = hash; - self.getData(peer, self.blockType, hash, next); - }, function(err) { - if (err) - return callback(err); + yield this.getData(peer, this.blockType, hash); + } - // Schedule the getdata's we just added. - self.scheduleRequests(peer); + // Schedule the getdata's we just added. + this.scheduleRequests(peer); - // Restart the getheaders process - // Technically `last` is not indexed yet so - // the locator hashes will not be entirely - // accurate. However, it shouldn't matter - // that much since FindForkInGlobalIndex - // simply tries to find the latest block in - // the peer's chain. - if (last && headers.length === 2000) - return peer.getHeaders(last, null, callback); - - callback(); - }); -}; + // Restart the getheaders process + // Technically `last` is not indexed yet so + // the locator hashes will not be entirely + // accurate. However, it shouldn't matter + // that much since FindForkInGlobalIndex + // simply tries to find the latest block in + // the peer's chain. + if (last && headers.length === 2000) + yield peer.getHeaders(last); +}); /** * Handle `inv` packet from peer (containing only BLOCK types). * @private * @param {Hash[]} hashes * @param {Peer} peer - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype._handleBlocks = function _handleBlocks(hashes, peer, callback) { - var self = this; +Pool.prototype._handleBlocks = co(function* _handleBlocks(hashes, peer) { + var i, hash, exists; assert(!this.options.headers); @@ -816,51 +819,45 @@ Pool.prototype._handleBlocks = function _handleBlocks(hashes, peer, callback) { this.startTimeout(); } - utils.forEachSerial(hashes, function(hash, next, i) { + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + // Resolve orphan chain. - if (self.chain.hasOrphan(hash)) { + if (this.chain.hasOrphan(hash)) { // There is a possible race condition here. // The orphan may get resolved by the time // we create the locator. In that case, we // should probably actually move to the // `exists` clause below if it is the last // hash. - self.logger.debug('Received known orphan hash (%s).', peer.hostname); - return peer.resolveOrphan(null, hash, next); + this.logger.debug('Received known orphan hash (%s).', peer.hostname); + yield peer.resolveOrphan(null, hash); + continue; } - self.getData(peer, self.blockType, hash, function(err, exists) { - if (err) - return next(err); + exists = yield this.getData(peer, this.blockType, hash); - // Normally we request the hashContinue. - // In the odd case where we already have - // it, we can do one of two things: either - // force re-downloading of the block to - // continue the sync, or do a getblocks - // from the last hash (this will reset - // the hashContinue on the remote node). - if (exists && i === hashes.length - 1) { - // Make sure we _actually_ have this block. - if (!self.requestMap[hash]) { - self.logger.debug('Received existing hash (%s).', peer.hostname); - return peer.getBlocks(hash, null, next); - } - // Otherwise, we're still requesting it. Ignore. - self.logger.debug('Received requested hash (%s).', peer.hostname); + // Normally we request the hashContinue. + // In the odd case where we already have + // it, we can do one of two things: either + // force re-downloading of the block to + // continue the sync, or do a getblocks + // from the last hash (this will reset + // the hashContinue on the remote node). + if (exists && i === hashes.length - 1) { + // Make sure we _actually_ have this block. + if (!this.requestMap[hash]) { + this.logger.debug('Received existing hash (%s).', peer.hostname); + yield peer.getBlocks(hash, null); + continue; } + // Otherwise, we're still requesting it. Ignore. + this.logger.debug('Received requested hash (%s).', peer.hostname); + } + } - next(); - }); - }, function(err) { - if (err) - return callback(err); - - self.scheduleRequests(peer); - - callback(); - }); -}; + this.scheduleRequests(peer); +}); /** * Handle `inv` packet from peer (containing only BLOCK types). @@ -868,46 +865,55 @@ Pool.prototype._handleBlocks = function _handleBlocks(hashes, peer, callback) { * @private * @param {Hash[]} hashes * @param {Peer} peer - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype._handleInv = function _handleInv(hashes, peer, callback) { - var self = this; +Pool.prototype._handleInv = co(function* _handleInv(hashes, peer) { + var unlock = yield this.locker.lock(); + try { + return yield this.__handleInv(hashes, peer); + } finally { + unlock(); + } +}); - callback = this._lock(_handleInv, [hashes, peer, callback]); +/** + * Handle `inv` packet from peer without a lock. + * @private + * @param {Hash[]} hashes + * @param {Peer} peer + * @returns {Promise} + */ - if (!callback) - return; +Pool.prototype.__handleInv = co(function* _handleInv(hashes, peer) { + var i, hash; // Ignore for now if we're still syncing if (!this.chain.synced && !peer.isLoader()) - return callback(); + return; - if (!this.options.headers) - return this._handleBlocks(hashes, peer, callback); + if (!this.options.headers) { + yield this._handleBlocks(hashes, peer); + return; + } - utils.forEachSerial(hashes, function(hash, next) { - peer.getHeaders(null, hash, next); - }, function(err) { - if (err) - return callback(err); + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + yield peer.getHeaders(null, hash); + } - self.scheduleRequests(peer); - - callback(); - }); -}; + this.scheduleRequests(peer); +}); /** * Handle `block` packet. Attempt to add to chain. * @private * @param {MemBlock|MerkleBlock} block * @param {Peer} peer - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype._handleBlock = function _handleBlock(block, peer, callback) { - var self = this; +Pool.prototype._handleBlock = co(function* _handleBlock(block, peer) { var requested; // Fulfill the load request. @@ -920,69 +926,66 @@ Pool.prototype._handleBlock = function _handleBlock(block, peer, callback) { this.logger.warning( 'Received unrequested block: %s (%s).', block.rhash, peer.hostname); - return utils.nextTick(callback); + return yield co.wait(); } - this.chain.add(block, function(err) { - if (err) { - if (err.type !== 'VerifyError') { - self.scheduleRequests(peer); - return callback(err); + try { + yield this.chain.add(block); + } catch (err) { + if (err.type !== 'VerifyError') { + this.scheduleRequests(peer); + throw err; + } + + if (err.score !== -1) + peer.reject(block, err.code, err.reason, err.score); + + if (err.reason === 'bad-prevblk') { + if (this.options.headers) { + peer.setMisbehavior(10); + throw err; } - - if (err.score !== -1) - peer.reject(block, err.code, err.reason, err.score); - - if (err.reason === 'bad-prevblk') { - if (self.options.headers) { - peer.setMisbehavior(10); - return callback(err); - } - self.logger.debug('Peer sent an orphan block. Resolving.'); - return peer.resolveOrphan(null, block.hash('hex'), function(e) { - self.scheduleRequests(peer); - return callback(e || err); - }); - } - - self.scheduleRequests(peer); - return callback(err); + this.logger.debug('Peer sent an orphan block. Resolving.'); + yield peer.resolveOrphan(null, block.hash('hex')); + this.scheduleRequests(peer); + throw err; } - self.scheduleRequests(peer); + this.scheduleRequests(peer); + throw err; + } - self.emit('chain-progress', self.chain.getProgress(), peer); + this.scheduleRequests(peer); - if (self.logger.level >= 4 && self.chain.total % 20 === 0) { - self.logger.debug('Status:' - + ' ts=%s height=%d highest=%d progress=%s' - + ' blocks=%d orphans=%d active=%d' - + ' queue=%d target=%s peers=%d' - + ' pending=%d jobs=%d', - utils.date(block.ts), - self.chain.height, - self.chain.bestHeight, - (self.chain.getProgress() * 100).toFixed(2) + '%', - self.chain.total, - self.chain.orphan.count, - self.activeBlocks, - peer.queueBlock.length, - block.bits, - self.peers.all.length, - self.chain.locker.pending.length, - self.chain.locker.jobs.length); - } + this.emit('chain-progress', this.chain.getProgress(), peer); - if (self.chain.total % 2000 === 0) { - self.logger.info( - 'Received 2000 more blocks (height=%d, hash=%s).', - self.chain.height, - block.rhash); - } + if (this.logger.level >= 4 && this.chain.total % 20 === 0) { + this.logger.debug('Status:' + + ' ts=%s height=%d highest=%d progress=%s' + + ' blocks=%d orphans=%d active=%d' + + ' queue=%d target=%s peers=%d' + + ' pending=%d jobs=%d', + utils.date(block.ts), + this.chain.height, + this.chain.bestHeight, + (this.chain.getProgress() * 100).toFixed(2) + '%', + this.chain.total, + this.chain.orphan.count, + this.activeBlocks, + peer.queueBlock.length, + block.bits, + this.peers.all.length, + this.chain.locker.pending.length, + this.chain.locker.jobs.length); + } - callback(); - }); -}; + if (this.chain.total % 2000 === 0) { + this.logger.info( + 'Received 2000 more blocks (height=%d, hash=%s).', + this.chain.height, + block.rhash); + } +}); /** * Send `mempool` to all peers. @@ -1025,9 +1028,9 @@ Pool.prototype.sendAlert = function sendAlert(alert) { Pool.prototype.createPeer = function createPeer(addr, socket) { var self = this; - var peer = new bcoin.peer(this, addr, socket); + var peer = new Peer(this, addr, socket); - peer.once('ack', function() { + peer.once('open', function() { if (!peer.outbound) return; @@ -1069,7 +1072,7 @@ Pool.prototype.createPeer = function createPeer(addr, socket) { self.addLoader(); }); - peer.on('merkleblock', function(block) { + peer.on('merkleblock', co(function* (block) { if (!self.options.spv) return; @@ -1078,18 +1081,20 @@ Pool.prototype.createPeer = function createPeer(addr, socket) { // If the peer sent us a block that was added // to the chain (not orphans), reset the timeout. - self._handleBlock(block, peer, function(err) { - if (err) - return self.emit('error', err); + try { + yield self._handleBlock(block, peer); + } catch (e) { + self.emit('error', e); + return; + } - if (peer.isLoader()) { - self.startInterval(); - self.startTimeout(); - } - }); - }); + if (peer.isLoader()) { + self.startInterval(); + self.startTimeout(); + } + })); - peer.on('block', function(block) { + peer.on('block', co(function* (block) { if (self.options.spv) return; @@ -1098,16 +1103,18 @@ Pool.prototype.createPeer = function createPeer(addr, socket) { // If the peer sent us a block that was added // to the chain (not orphans), reset the timeout. - self._handleBlock(block, peer, function(err) { - if (err) - return self.emit('error', err); + try { + yield self._handleBlock(block, peer); + } catch (e) { + self.emit('error', e); + return; + } - if (peer.isLoader()) { - self.startInterval(); - self.startTimeout(); - } - }); - }); + if (peer.isLoader()) { + self.startInterval(); + self.startTimeout(); + } + })); peer.on('error', function(err) { self.emit('error', err, peer); @@ -1150,12 +1157,13 @@ Pool.prototype.createPeer = function createPeer(addr, socket) { } }); - peer.on('tx', function(tx) { - self._handleTX(tx, peer, function(err) { - if (err) - self.emit('error', err); - }); - }); + peer.on('tx', co(function* (tx) { + try { + yield self._handleTX(tx, peer); + } catch (e) { + self.emit('error', e); + } + })); peer.on('addr', function(addrs) { var i, addr; @@ -1187,7 +1195,7 @@ Pool.prototype.createPeer = function createPeer(addr, socket) { self.fillPeers(); }); - peer.on('txs', function(txs) { + peer.on('txs', co(function* (txs) { var i, hash; self.emit('txs', txs, peer); @@ -1197,9 +1205,13 @@ Pool.prototype.createPeer = function createPeer(addr, socket) { for (i = 0; i < txs.length; i++) { hash = txs[i]; - self.getData(peer, self.txType, hash); + try { + yield self.getData(peer, self.txType, hash); + } catch (e) { + self.emit('error', e); + } } - }); + })); peer.on('version', function(version) { self.logger.info( @@ -1210,30 +1222,32 @@ Pool.prototype.createPeer = function createPeer(addr, socket) { version.services.toString(2), version.agent); - bcoin.time.add(peer.hostname, version.ts); + time.add(peer.hostname, version.ts); self.emit('version', version, peer); }); - peer.on('headers', function(headers) { + peer.on('headers', co(function* (headers) { if (!self.syncing) return; - self._handleHeaders(headers, peer, function(err) { - if (err) - self.emit('error', err); - }); - }); + try { + yield self._handleHeaders(headers, peer); + } catch (e) { + self.emit('error', e); + } + })); - peer.on('blocks', function(hashes) { + peer.on('blocks', co(function* (hashes) { if (!self.syncing) return; - self._handleInv(hashes, peer, function(err) { - if (err) - self.emit('error', err); - }); - }); + try { + yield self._handleInv(hashes, peer); + } catch (e) { + self.emit('error', e); + } + })); return peer; }; @@ -1246,7 +1260,7 @@ Pool.prototype.createPeer = function createPeer(addr, socket) { */ Pool.prototype._handleAlert = function _handleAlert(alert, peer) { - var now = bcoin.now(); + var now = time.now(); if (!alert.verify(this.network.alertKey)) { this.logger.warning('Peer sent a phony alert packet (%s).', peer.hostname); @@ -1280,7 +1294,7 @@ Pool.prototype._handleAlert = function _handleAlert(alert, peer) { } // Keep alert disabled on main. - if (this.network === bcoin.network.main) { + if (this.network === Network.main) { // https://github.com/bitcoin/bitcoin/pull/7692#issuecomment-197967429 this.logger.warning('The Japanese government sent an alert packet.'); this.logger.warning('Here is their IP: %s.', peer.hostname); @@ -1316,14 +1330,11 @@ Pool.prototype.hasReject = function hasReject(hash) { * @private * @param {TX} tx * @param {Peer} peer - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype._handleTX = function _handleTX(tx, peer, callback) { - var self = this; - var i, requested; - - callback = utils.ensure(callback); +Pool.prototype._handleTX = co(function* _handleTX(tx, peer) { + var i, requested, missing; // Fulfill the load request. requested = this.fulfill(tx); @@ -1338,38 +1349,35 @@ Pool.prototype._handleTX = function _handleTX(tx, peer, callback) { tx.rhash, peer.hostname); if (this.hasReject(tx.hash())) { - return callback(new VerifyError(tx, + throw new VerifyError(tx, 'alreadyknown', 'txn-already-in-mempool', - 0)); + 0); } } if (!this.mempool) { this.emit('tx', tx, peer); - return callback(); + return; } - this.mempool.addTX(tx, function(err, missing) { - if (err) { - if (err.type === 'VerifyError') { - if (err.score !== -1) - peer.reject(tx, err.code, err.reason, err.score); - return callback(err); - } - return callback(err); + try { + missing = yield this.mempool.addTX(tx); + } catch (err) { + if (err.type === 'VerifyError') { + if (err.score !== -1) + peer.reject(tx, err.code, err.reason, err.score); } + throw err; + } - if (missing) { - for (i = 0; i < missing.length; i++) - self.getData(peer, self.txType, missing[i]); - } + if (this.options.requestMissing && missing) { + for (i = 0; i < missing.length; i++) + yield this.getData(peer, this.txType, missing[i]); + } - self.emit('tx', tx, peer); - - callback(); - }); -}; + this.emit('tx', tx, peer); +}); /** * Create a leech peer from an existing socket. @@ -1523,7 +1531,7 @@ Pool.prototype.updateWatch = function updateWatch() { */ Pool.prototype.watchAddress = function watchAddress(address) { - this.watch(bcoin.address.getHash(address)); + this.watch(Address.getHash(address)); }; /** @@ -1532,114 +1540,102 @@ Pool.prototype.watchAddress = function watchAddress(address) { * @param {Peer} peer * @param {Number} type - `getdata` type (see {@link constants.inv}). * @param {Hash} hash - {@link Block} or {@link TX} hash. - * @param {Object?} options - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype.getData = function getData(peer, type, hash, callback) { +Pool.prototype.getData = co(function* getData(peer, type, hash) { var self = this; - var item; - - callback = utils.ensure(callback); + var item, exists; if (!this.loaded) - return callback(); + return; - this.has(peer, type, hash, function(err, exists) { - if (err) - return callback(err); + exists = yield this.has(peer, type, hash); - if (exists) - return callback(null, true); + if (exists) + return true; - item = new LoadRequest(self, peer, type, hash); + item = new LoadRequest(this, peer, type, hash); - if (type === self.txType) { - if (peer.queueTX.length === 0) { - utils.nextTick(function() { - self.logger.debug( - 'Requesting %d/%d txs from peer with getdata (%s).', - peer.queueTX.length, - self.activeTX, - peer.hostname); + if (type === this.txType) { + if (peer.queueTX.length === 0) { + utils.nextTick(function() { + self.logger.debug( + 'Requesting %d/%d txs from peer with getdata (%s).', + peer.queueTX.length, + self.activeTX, + peer.hostname); - peer.getData(peer.queueTX); - peer.queueTX.length = 0; - }); - } - - peer.queueTX.push(item.start()); - - return callback(null, false); + peer.getData(peer.queueTX); + peer.queueTX.length = 0; + }); } - peer.queueBlock.push(item); + peer.queueTX.push(item.start()); - callback(null, false); - }); -}; + return false; + } + + peer.queueBlock.push(item); + + return false; +}); /** * Test whether the pool has or has seen an item. * @param {Peer} peer * @param {InvType} type * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Pool.prototype.has = function has(peer, type, hash, callback) { - var self = this; +Pool.prototype.has = co(function* has(peer, type, hash) { + var exists = yield this.exists(type, hash); - this.exists(type, hash, function(err, exists) { - if (err) - return callback(err); + if (exists) + return true; - if (exists) - return callback(null, true); + // Check the pending requests. + if (this.requestMap[hash]) + return true; - // Check the pending requests. - if (self.requestMap[hash]) - return callback(null, true); + if (type !== this.txType) + return false; - if (type !== self.txType) - return callback(null, false); + // If we recently rejected this item. Ignore. + if (this.hasReject(hash)) { + this.logger.spam( + 'Peer sent a known reject of %s (%s).', + utils.revHex(hash), peer.hostname); + return true; + } - // If we recently rejected this item. Ignore. - if (self.hasReject(hash)) { - self.logger.spam( - 'Peer sent a known reject of %s (%s).', - utils.revHex(hash), peer.hostname); - return callback(null, true); - } - - return callback(null, false); - }); -}; + return false; +}); /** * Test whether the chain or mempool has seen an item. * @param {InvType} type * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Pool.prototype.exists = function exists(type, hash, callback) { +Pool.prototype.exists = function exists(type, hash) { if (type === this.txType) { // Check the TX filter if // we don't have a mempool. if (!this.mempool) { - callback = utils.asyncify(callback); if (this.txFilter.added(hash, 'hex')) - return callback(null, false); - return callback(null, true); + return Promise.resolve(false); + return Promise.resolve(true); } // Check the mempool. - return callback(null, this.mempool.has(hash)); + return Promise.resolve(this.mempool.has(hash)); } // Check the chain. - this.chain.has(hash, callback); + return this.chain.has(hash); }; /** @@ -1647,21 +1643,18 @@ Pool.prototype.exists = function exists(type, hash, callback) { * @param {Peer} peer */ -Pool.prototype.scheduleRequests = function scheduleRequests(peer) { - var self = this; - +Pool.prototype.scheduleRequests = co(function* scheduleRequests(peer) { if (this.scheduled) return; this.scheduled = true; - this.chain.onDrain(function() { - utils.nextTick(function() { - self.sendRequests(peer); - self.scheduled = false; - }); - }); -}; + yield this.chain.onDrain(); + yield co.wait(); + + this.sendRequests(peer); + this.scheduled = false; +}); /** * Send scheduled requests in the request queues. @@ -1681,6 +1674,7 @@ Pool.prototype.sendRequests = function sendRequests(peer) { if (this.options.spv) { if (this.activeBlocks >= 500) return; + items = peer.queueBlock.slice(); peer.queueBlock.length = 0; } else { @@ -1726,12 +1720,12 @@ Pool.prototype.fulfill = function fulfill(data) { /** * Broadcast a transaction or block. * @param {TX|Block|InvItem} msg - * @param {Function} callback - Returns [Error]. Executes on request, reject, + * @returns {Promise} * or timeout. * @returns {BroadcastItem} */ -Pool.prototype.broadcast = function broadcast(msg, callback) { +Pool.prototype.broadcast = function broadcast(msg) { var hash = msg.hash; var item; @@ -1743,16 +1737,15 @@ Pool.prototype.broadcast = function broadcast(msg, callback) { if (item) { item.refresh(); item.announce(); - item.addCallback(callback); - return item; + } else { + item = new BroadcastItem(this, msg); + item.start(); + item.announce(); } - item = new BroadcastItem(this, msg, callback); - - item.start(); - item.announce(); - - return item; + return new Promise(function(resolve, reject) { + item.addCallback(co.wrap(resolve, reject)); + }); }; /** @@ -1881,66 +1874,63 @@ Pool.prototype.isIgnored = function isIgnored(addr) { /** * Attempt to retrieve external IP from icanhazip.com. - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype.getIP = function getIP(callback) { - var self = this; - var request, ip; +Pool.prototype.getIP = co(function* getIP() { + var request, res, ip; if (utils.isBrowser) - return callback(new Error('Could not find IP.')); + throw new Error('Could not find IP.'); request = require('../http/request'); - request({ - method: 'GET', - uri: 'http://icanhazip.com', - expect: 'text', - timeout: 3000 - }, function(err, res, body) { - if (err) - return self.getIP2(callback); + try { + res = yield request.promise({ + method: 'GET', + uri: 'http://icanhazip.com', + expect: 'text', + timeout: 3000 + }); + } catch (e) { + return yield this.getIP2(); + } - ip = body.trim(); + ip = res.body.trim(); - if (IP.version(ip) === -1) - return self.getIP2(callback); + if (IP.version(ip) === -1) + return yield this.getIP2(); - callback(null, IP.normalize(ip)); - }); -}; + return IP.normalize(ip); +}); /** * Attempt to retrieve external IP from dyndns.org. - * @param {Function} callback + * @returns {Promise} */ -Pool.prototype.getIP2 = function getIP2(callback) { - var request, ip; +Pool.prototype.getIP2 = co(function* getIP2() { + var request, res, ip; if (utils.isBrowser) - return callback(new Error('Could not find IP.')); + throw new Error('Could not find IP.'); request = require('../http/request'); - request({ + res = yield request.promise({ method: 'GET', uri: 'http://checkip.dyndns.org', expect: 'html', timeout: 3000 - }, function(err, res, body) { - if (err) - return callback(err); - - ip = /IP Address:\s*([0-9a-f.:]+)/i.exec(body); - - if (!ip || IP.version(ip[1]) === -1) - return callback(new Error('Could not find IP.')); - - callback(null, IP.normalize(ip[1])); }); -}; + + ip = /IP Address:\s*([0-9a-f.:]+)/i.exec(res.body); + + if (!ip || IP.version(ip[1]) === -1) + throw new Error('Could not find IP.'); + + return IP.normalize(ip[1]); +}); /** * Peer List @@ -2300,7 +2290,7 @@ HostList.prototype.addSeed = function addSeed(hostname) { * @param {Peer} peer * @param {Number} type - `getdata` type (see {@link constants.inv}). * @param {Hash} hash - * @param {Function} callback + * @returns {Promise} */ function LoadRequest(pool, peer, type, hash, callback) { @@ -2347,7 +2337,7 @@ LoadRequest.prototype._onTimeout = function _onTimeout() { /** * Add a callback to be executed when item is received. - * @param {Function} callback + * @returns {Promise} */ LoadRequest.prototype.addCallback = function addCallback(callback) { @@ -2454,7 +2444,7 @@ function BroadcastItem(pool, item, callback) { this.id = this.pool.uid++; this.msg = null; - if (item instanceof bcoin.tx) + if (item instanceof TX) assert(!item.mutable, 'Cannot broadcast mutable TX.'); if (item.toInv) { @@ -2482,7 +2472,7 @@ utils.inherits(BroadcastItem, EventEmitter); /** * Add a callback to be executed on ack, timeout, or reject. - * @param {Function} callback + * @returns {Promise} */ BroadcastItem.prototype.addCallback = function addCallback(callback) { @@ -2513,7 +2503,7 @@ BroadcastItem.prototype.start = function start() { BroadcastItem.prototype.refresh = function refresh() { var self = this; - if (this.timeout) { + if (this.timeout != null) { clearTimeout(this.timeout); this.timeout = null; } diff --git a/lib/net/proxysocket.js b/lib/net/proxysocket.js index 2c988591..3e64ea43 100644 --- a/lib/net/proxysocket.js +++ b/lib/net/proxysocket.js @@ -1,10 +1,9 @@ 'use strict'; -var bcoin = require('../env'); -var utils = bcoin.utils; +var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); var BufferWriter = require('../utils/writer'); -var assert = utils.assert; +var assert = require('assert'); var EventEmitter = require('events').EventEmitter; var IOClient = require('socket.io-client'); @@ -18,6 +17,8 @@ function ProxySocket(uri) { this.socket = new IOClient(uri, { reconnection: false }); this.sendBuffer = []; + this.recvBuffer = []; + this.paused = false; this.snonce = null; this.bytesWritten = 0; this.bytesRead = 0; @@ -60,6 +61,10 @@ ProxySocket.prototype._init = function _init() { this.socket.on('tcp data', function(data) { data = new Buffer(data, 'hex'); + if (self.paused) { + self.recvBuffer.push(data); + return; + } self.bytesRead += data.length; self.emit('data', data); }); @@ -129,9 +134,13 @@ ProxySocket.prototype.connect = function connect(port, host) { this.sendBuffer.length = 0; }; -ProxySocket.prototype.write = function write(data) { +ProxySocket.prototype.write = function write(data, callback) { if (!this.info) { this.sendBuffer.push(data); + + if (callback) + callback(); + return true; } @@ -139,9 +148,33 @@ ProxySocket.prototype.write = function write(data) { this.socket.emit('tcp data', data.toString('hex')); + if (callback) + callback(); + return true; }; +ProxySocket.prototype.pause = function pause() { + this.paused = true; + this.socket.emit('tcp pause'); +}; + +ProxySocket.prototype.resume = function resume() { + var recv = this.recvBuffer; + var i, data; + + this.paused = false; + this.recvBuffer = []; + + for (i = 0; i < recv.length; i++) { + data = recv[i]; + this.bytesRead += data.length; + this.emit('data', data); + } + + this.socket.emit('tcp resume'); +}; + ProxySocket.prototype.destroy = function destroy() { if (this.closed) return; diff --git a/lib/net/timedata.js b/lib/net/timedata.js index 81b1b009..a3ed3b54 100644 --- a/lib/net/timedata.js +++ b/lib/net/timedata.js @@ -112,4 +112,4 @@ function compare(a, b) { * Expose */ -module.exports = TimeData; +module.exports = new TimeData(); diff --git a/lib/node/config.js b/lib/node/config.js index 991a6540..07885290 100644 --- a/lib/node/config.js +++ b/lib/node/config.js @@ -8,7 +8,7 @@ var Network = require('../protocol/network'); var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); var fs; if (!utils.isBrowser) diff --git a/lib/node/fullnode.js b/lib/node/fullnode.js index c5d2f60c..e92ea094 100644 --- a/lib/node/fullnode.js +++ b/lib/node/fullnode.js @@ -7,10 +7,23 @@ 'use strict'; -var bcoin = require('../env'); -var constants = bcoin.constants; +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); -var Node = bcoin.node; +var co = require('../utils/co'); +var Node = require('./node'); +var Chain = require('../chain/chain'); +var Fees = require('../mempool/fees'); +var Mempool = require('../mempool/mempool'); +var Pool = require('../net/pool'); +var Miner = require('../miner/miner'); +var WalletDB = require('../wallet/walletdb'); +var HTTPServer; + +try { + HTTPServer = require('../http/server'); +} catch (e) { + ; +} /** * Create a fullnode complete with a chain, @@ -53,7 +66,7 @@ function Fullnode(options) { Node.call(this, options); // Instantiate blockchain. - this.chain = new bcoin.chain({ + this.chain = new Chain({ network: this.network, logger: this.logger, db: this.options.db, @@ -70,13 +83,13 @@ function Fullnode(options) { }); // Fee estimation. - this.fees = new bcoin.fees( + this.fees = new Fees( constants.tx.MIN_RELAY, this.network, this.logger); // Mempool needs access to the chain. - this.mempool = new bcoin.mempool({ + this.mempool = new Mempool({ network: this.network, logger: this.logger, chain: this.chain, @@ -90,7 +103,7 @@ function Fullnode(options) { }); // Pool needs access to the chain and mempool. - this.pool = new bcoin.pool({ + this.pool = new Pool({ network: this.network, logger: this.logger, chain: this.chain, @@ -115,19 +128,18 @@ function Fullnode(options) { }); // Miner needs access to the chain and mempool. - this.miner = new bcoin.miner({ + this.miner = new Miner({ network: this.network, logger: this.logger, chain: this.chain, mempool: this.mempool, fees: this.fees, address: this.options.payoutAddress, - coinbaseFlags: this.options.coinbaseFlags, - parallel: this.options.parallel + coinbaseFlags: this.options.coinbaseFlags }); // Wallet database needs access to fees. - this.walletdb = new bcoin.walletdb({ + this.walletdb = new WalletDB({ network: this.network, logger: this.logger, fees: this.fees, @@ -141,7 +153,7 @@ function Fullnode(options) { // HTTP needs access to the node. if (!utils.isBrowser) { - this.http = new bcoin.http.server({ + this.http = new HTTPServer({ network: this.network, logger: this.logger, node: this, @@ -185,7 +197,7 @@ Fullnode.prototype._init = function _init() { this.mempool.on('tx', function(tx) { self.emit('tx', tx); - self.walletdb.addTX(tx, onError); + self.walletdb.addTX(tx).catch(onError); }); this.chain.on('block', function(block) { @@ -193,17 +205,17 @@ Fullnode.prototype._init = function _init() { }); this.chain.on('connect', function(entry, block) { - self.walletdb.addBlock(entry, block.txs, onError); + self.walletdb.addBlock(entry, block.txs).catch(onError); if (self.chain.synced) - self.mempool.addBlock(block, onError); + self.mempool.addBlock(block).catch(onError); }); this.chain.on('disconnect', function(entry, block) { - self.walletdb.removeBlock(entry, onError); + self.walletdb.removeBlock(entry).catch(onError); if (self.chain.synced) - self.mempool.removeBlock(block, onError); + self.mempool.removeBlock(block).catch(onError); }); this.miner.on('block', function(block) { @@ -211,7 +223,7 @@ Fullnode.prototype._init = function _init() { }); this.walletdb.on('send', function(tx) { - self.sendTX(tx, onError); + self.sendTX(tx).catch(onError); }); }; @@ -219,82 +231,70 @@ Fullnode.prototype._init = function _init() { * Open the node and all its child objects, * wait for the database to load. * @alias Fullnode#open - * @param {Function} callback + * @returns {Promise} */ -Fullnode.prototype._open = function open(callback) { - var self = this; +Fullnode.prototype._open = co(function* open() { + yield this.chain.open(); + yield this.mempool.open(); + yield this.miner.open(); + yield this.pool.open(); + yield this.walletdb.open(); - utils.serial([ - this.chain.open.bind(this.chain), - this.mempool.open.bind(this.mempool), - this.miner.open.bind(this.miner), - this.pool.open.bind(this.pool), - this.walletdb.open.bind(this.walletdb), - // Ensure primary wallet. - this.openWallet.bind(this), - // Rescan for any missed transactions. - this.rescan.bind(this), - // Rebroadcast pending transactions. - this.resend.bind(this), - function(next) { - if (!self.http) - return next(); - self.http.open(next); - } - ], function(err) { - if (err) - return callback(err); + // Ensure primary wallet. + yield this.openWallet(); - self.logger.info('Node is loaded.'); + // Rescan for any missed transactions. + yield this.rescan(); - callback(); - }); -}; + // Rebroadcast pending transactions. + yield this.resend(); + + if (this.http) + yield this.http.open(); + + this.logger.info('Node is loaded.'); +}); /** * Close the node, wait for the database to close. * @alias Fullnode#close - * @param {Function} callback + * @returns {Promise} */ -Fullnode.prototype._close = function close(callback) { - var self = this; +Fullnode.prototype._close = co(function* close() { + if (this.http) + yield this.http.close(); + + yield this.wallet.destroy(); this.wallet = null; - utils.serial([ - function(next) { - if (!self.http) - return next(); - self.http.close(next); - }, - this.walletdb.close.bind(this.walletdb), - this.pool.close.bind(this.pool), - this.miner.close.bind(this.miner), - this.mempool.close.bind(this.mempool), - this.chain.close.bind(this.chain) - ], callback); -}; + yield this.walletdb.close(); + yield this.pool.close(); + yield this.miner.close(); + yield this.mempool.close(); + yield this.chain.close(); + + this.logger.info('Node is closed.'); +}); /** * Rescan for any missed transactions. - * @param {Function} callback + * @returns {Promise} */ -Fullnode.prototype.rescan = function rescan(callback) { +Fullnode.prototype.rescan = function rescan() { if (this.options.noScan) { - this.walletdb.setTip( + return this.walletdb.setTip( this.chain.tip.hash, - this.chain.height, - callback); - return; + this.chain.height); } // Always rescan to make sure we didn't // miss anything: there is no atomicity // between the chaindb and walletdb. - this.walletdb.rescan(this.chain.db, callback); + return this.walletdb.rescan(this.chain.db); }; /** @@ -302,7 +302,7 @@ Fullnode.prototype.rescan = function rescan(callback) { * by the mempool - use with care, lest you get banned from * bitcoind nodes). * @param {TX|Block} item - * @param {Function} callback + * @returns {Promise} */ Fullnode.prototype.broadcast = function broadcast(item, callback) { @@ -316,53 +316,35 @@ Fullnode.prototype.broadcast = function broadcast(item, callback) { * node.sendTX(tx, callback); * node.sendTX(tx, true, callback); * @param {TX} tx - * @param {Boolean?} wait - Wait to execute callback until a node - * requests our TX, rejects it, or the broadcast itself times out. - * @param {Function} callback - Returns [{@link VerifyError}|Error]. */ -Fullnode.prototype.sendTX = function sendTX(tx, wait, callback) { - var self = this; - - if (!callback) { - callback = wait; - wait = null; +Fullnode.prototype.sendTX = co(function* sendTX(tx) { + try { + yield this.mempool.addTX(tx); + } catch (err) { + if (err.type === 'VerifyError') { + this._error(err); + this.logger.warning('Verification failed for tx: %s.', tx.rhash); + this.logger.warning('Attempting to broadcast anyway...'); + yield this.pool.broadcast(tx); + return; + } + throw err; } - this.mempool.addTX(tx, function(err) { - if (err) { - if (err.type === 'VerifyError') { - self._error(err); - self.logger.warning('Verification failed for tx: %s.', tx.rhash); - self.logger.warning('Attempting to broadcast anyway...'); - if (!wait) { - self.pool.broadcast(tx); - return callback(); - } - return self.pool.broadcast(tx, callback); - } - return callback(err); - } + if (!this.options.selfish) + tx = tx.toInv(); - if (!self.options.selfish) - tx = tx.toInv(); - - if (!wait) { - self.pool.broadcast(tx); - return callback(); - } - - self.pool.broadcast(tx, callback); - }); -}; + yield this.pool.broadcast(tx); +}); /** * Listen on a server socket on * the p2p network (accepts leech peers). */ -Fullnode.prototype.listen = function listen(callback) { - this.pool.listen(callback); +Fullnode.prototype.listen = function listen() { + return this.pool.listen(); }; /** @@ -392,21 +374,21 @@ Fullnode.prototype.stopSync = function stopSync() { /** * Retrieve a block from the chain database. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -Fullnode.prototype.getBlock = function getBlock(hash, callback) { - this.chain.db.getBlock(hash, callback); +Fullnode.prototype.getBlock = function getBlock(hash) { + return this.chain.db.getBlock(hash); }; /** * Retrieve a block from the chain database, filled with coins. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link Block}]. + * @returns {Promise} - Returns {@link Block}. */ -Fullnode.prototype.getFullBlock = function getFullBlock(hash, callback) { - this.chain.db.getFullBlock(hash, callback); +Fullnode.prototype.getFullBlock = function getFullBlock(hash) { + return this.chain.db.getFullBlock(hash); }; /** @@ -414,139 +396,130 @@ Fullnode.prototype.getFullBlock = function getFullBlock(hash, callback) { * Takes into account spent coins in the mempool. * @param {Hash} hash * @param {Number} index - * @param {Function} callback - Returns [Error, {@link Coin}]. + * @returns {Promise} - Returns {@link Coin}. */ -Fullnode.prototype.getCoin = function getCoin(hash, index, callback) { +Fullnode.prototype.getCoin = function getCoin(hash, index) { var coin = this.mempool.getCoin(hash, index); if (coin) - return callback(null, coin); + return Promise.resolve(coin); if (this.mempool.isSpent(hash, index)) - return callback(); + return Promise.resolve(null); - this.chain.db.getCoin(hash, index, callback); + return this.chain.db.getCoin(hash, index); }; /** * Get coins that pertain to an address from the mempool or chain database. * Takes into account spent coins in the mempool. * @param {Address} addresses - * @param {Function} callback - Returns [Error, {@link Coin}[]]. + * @returns {Promise} - Returns {@link Coin}[]. */ -Fullnode.prototype.getCoinsByAddress = function getCoinsByAddress(addresses, callback) { - var self = this; +Fullnode.prototype.getCoinsByAddress = co(function* getCoinsByAddress(addresses) { var coins = this.mempool.getCoinsByAddress(addresses); - var i, coin, spent; + var i, blockCoins, coin, spent; - this.chain.db.getCoinsByAddress(addresses, function(err, blockCoins) { - if (err) - return callback(err); + blockCoins = yield this.chain.db.getCoinsByAddress(addresses); - for (i = 0; i < blockCoins.length; i++) { - coin = blockCoins[i]; - spent = self.mempool.isSpent(coin.hash, coin.index); + for (i = 0; i < blockCoins.length; i++) { + coin = blockCoins[i]; + spent = this.mempool.isSpent(coin.hash, coin.index); - if (!spent) - coins.push(coin); - } + if (!spent) + coins.push(coin); + } - callback(null, coins); - }); -}; + return coins; +}); /** * Retrieve transactions pertaining to an * address from the mempool or chain database. * @param {Address} addresses - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -Fullnode.prototype.getTXByAddress = function getTXByAddress(addresses, callback) { +Fullnode.prototype.getTXByAddress = co(function* getTXByAddress(addresses) { var mempool = this.mempool.getTXByAddress(addresses); - - this.chain.db.getTXByAddress(addresses, function(err, txs) { - if (err) - return callback(err); - - callback(null, mempool.concat(txs)); - }); -}; + var txs = yield this.chain.db.getTXByAddress(addresses); + return mempool.concat(txs); +}); /** * Retrieve a transaction from the mempool or chain database. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Fullnode.prototype.getTX = function getTX(hash, callback) { +Fullnode.prototype.getTX = function getTX(hash) { var tx = this.mempool.getTX(hash); if (tx) - return callback(null, tx); + return Promise.resolve(tx); - this.chain.db.getTX(hash, callback); + return this.chain.db.getTX(hash); }; /** * Test whether the mempool or chain contains a transaction. * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Fullnode.prototype.hasTX = function hasTX(hash, callback) { +Fullnode.prototype.hasTX = function hasTX(hash) { if (this.mempool.hasTX(hash)) - return callback(null, true); + return Promise.resolve(true); - this.chain.db.hasTX(hash, callback); + return this.chain.db.hasTX(hash); }; /** * Check whether a coin has been spent. * @param {Hash} hash * @param {Number} index - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Fullnode.prototype.isSpent = function isSpent(hash, index, callback) { +Fullnode.prototype.isSpent = function isSpent(hash, index) { if (this.mempool.isSpent(hash, index)) - return callback(null, true); + return Promise.resolve(true); - this.chain.db.isSpent(hash, index, callback); + return this.chain.db.isSpent(hash, index); }; /** * Fill a transaction with coins from the mempool * and chain database (unspent only). * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Fullnode.prototype.fillCoins = function fillCoins(tx, callback) { - this.mempool.fillAllCoins(tx, callback); +Fullnode.prototype.fillCoins = function fillCoins(tx) { + return this.mempool.fillAllCoins(tx); }; /** * Fill a transaction with all historical coins * from the mempool and chain database. * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Fullnode.prototype.fillHistory = function fillHistory(tx, callback) { - this.mempool.fillAllHistory(tx, callback); +Fullnode.prototype.fillHistory = function fillHistory(tx) { + return this.mempool.fillAllHistory(tx); }; /** * Return bitcoinj-style confidence for a transaction. * @param {Hash|TX} tx - * @param {Function} callback - Returns [Error, {@link Confidence}]. + * @returns {Promise} - Returns {@link Confidence}. */ -Fullnode.prototype.getConfidence = function getConfidence(tx, callback) { - this.mempool.getConfidence(tx, callback); +Fullnode.prototype.getConfidence = function getConfidence(tx) { + return this.mempool.getConfidence(tx); }; /* diff --git a/lib/node/index.js b/lib/node/index.js new file mode 100644 index 00000000..dde64cf7 --- /dev/null +++ b/lib/node/index.js @@ -0,0 +1,7 @@ +'use strict'; + +exports.config = require('./config'); +exports.Fullnode = require('./fullnode'); +exports.Logger = require('./logger'); +exports.Node = require('./node'); +exports.SPVNode = require('./spvnode'); diff --git a/lib/node/logger.js b/lib/node/logger.js index 54329110..7c06da3f 100644 --- a/lib/node/logger.js +++ b/lib/node/logger.js @@ -349,6 +349,12 @@ Logger.prototype.memory = function memory() { utils.mb(mem.rss - mem.heapTotal)); }; +/* + * Default + */ + +Logger.global = new Logger('none'); + /* * Expose */ diff --git a/lib/node/node.js b/lib/node/node.js index d44fe122..6c6174e5 100644 --- a/lib/node/node.js +++ b/lib/node/node.js @@ -7,10 +7,14 @@ 'use strict'; -var bcoin = require('../env'); var AsyncObject = require('../utils/async'); var utils = require('../utils/utils'); -var assert = utils.assert; +var co = require('../utils/co'); +var assert = require('assert'); +var Network = require('../protocol/network'); +var Logger = require('./logger'); +var time = require('../net/timedata'); +var workers = require('../workers/workers'); /** * Base class from which every other @@ -33,7 +37,7 @@ function Node(options) { this.parseOptions(options); this.options = options; - this.network = bcoin.network.get(options.network); + this.network = Network.get(options.network); this.prefix = options.prefix; this.logger = options.logger; @@ -62,7 +66,7 @@ Node.prototype.__init = function __init() { var self = this; if (!this.logger) { - this.logger = new bcoin.logger({ + this.logger = new Logger({ level: this.options.logLevel || 'none', console: this.options.logConsole, file: this.options.logFile @@ -87,28 +91,28 @@ Node.prototype._onOpen = function _onOpen() { this.logger.open(); - this._bind(bcoin.time, 'offset', function(offset) { + this._bind(time, 'offset', function(offset) { self.logger.info('Time offset: %d (%d minutes).', offset, offset / 60 | 0); }); - this._bind(bcoin.time, 'sample', function(sample, total) { + this._bind(time, 'sample', function(sample, total) { self.logger.debug('Added time data: samples=%d, offset=%d (%d minutes).', total, sample, sample / 60 | 0); }); - this._bind(bcoin.time, 'mismatch', function() { + this._bind(time, 'mismatch', function() { self.logger.warning('Please make sure your system clock is correct!'); }); - this._bind(bcoin.workerPool, 'spawn', function(child) { + this._bind(workers.pool, 'spawn', function(child) { self.logger.info('Spawning worker process: %d.', child.id); }); - this._bind(bcoin.workerPool, 'exit', function(code, child) { + this._bind(workers.pool, 'exit', function(code, child) { self.logger.warning('Worker %d exited: %s.', child.id, code); }); - this._bind(bcoin.workerPool, 'error', function(err, child) { + this._bind(workers.pool, 'error', function(err, child) { if (child) { self.logger.error('Worker %d error: %s', child.id, err.message); return; @@ -179,7 +183,7 @@ Node.prototype._error = function _error(err) { */ Node.prototype.parseOptions = function parseOptions(options) { - options.network = bcoin.network.get(options.network); + options.network = Network.get(options.network); if (!options.prefix) options.prefix = utils.HOME + '/.bcoin'; @@ -229,12 +233,11 @@ Node.prototype.location = function location(name) { /** * Open and ensure primary wallet. - * @param {Function} callback + * @returns {Promise} */ -Node.prototype.openWallet = function openWallet(callback) { - var self = this; - var options; +Node.prototype.openWallet = co(function* openWallet() { + var options, wallet; assert(!this.wallet); @@ -243,34 +246,29 @@ Node.prototype.openWallet = function openWallet(callback) { passphrase: this.options.passphrase }; - this.walletdb.ensure(options, function(err, wallet) { - if (err) - return callback(err); + wallet = yield this.walletdb.ensure(options); - self.logger.info( - 'Loaded wallet with id=%s wid=%d address=%s', - wallet.id, wallet.wid, wallet.getAddress()); + this.logger.info( + 'Loaded wallet with id=%s wid=%d address=%s', + wallet.id, wallet.wid, wallet.getAddress()); - // Set the miner payout address if the - // programmer didn't pass one in. - if (self.miner) { - if (!self.options.payoutAddress) - self.miner.address = wallet.getAddress(); - } + // Set the miner payout address if the + // programmer didn't pass one in. + if (this.miner) { + if (!this.options.payoutAddress) + this.miner.address = wallet.getAddress(); + } - self.wallet = wallet; - - callback(); - }); -}; + this.wallet = wallet; +}); /** * Resend all pending transactions. - * @param {Function} callback + * @returns {Promise} */ -Node.prototype.resend = function resend(callback) { - this.walletdb.resend(callback); +Node.prototype.resend = function resend() { + return this.walletdb.resend(); }; /* diff --git a/lib/node/spvnode.js b/lib/node/spvnode.js index 95bf08e0..87be0fce 100644 --- a/lib/node/spvnode.js +++ b/lib/node/spvnode.js @@ -7,9 +7,19 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); -var Node = bcoin.node; +var co = require('../utils/co'); +var Node = require('./node'); +var Chain = require('../chain/chain'); +var Pool = require('../net/pool'); +var WalletDB = require('../wallet/walletdb'); +var HTTPServer; + +try { + HTTPServer = require('../http/server'); +} catch (e) { + ; +} /** * Create an spv node which only maintains @@ -40,7 +50,7 @@ function SPVNode(options) { Node.call(this, options); - this.chain = new bcoin.chain({ + this.chain = new Chain({ network: this.network, logger: this.logger, db: this.options.db, @@ -51,7 +61,7 @@ function SPVNode(options) { spv: true }); - this.pool = new bcoin.pool({ + this.pool = new Pool({ network: this.network, logger: this.logger, chain: this.chain, @@ -69,7 +79,7 @@ function SPVNode(options) { spv: true }); - this.walletdb = new bcoin.walletdb({ + this.walletdb = new WalletDB({ network: this.network, logger: this.logger, db: this.options.db, @@ -80,7 +90,7 @@ function SPVNode(options) { }); if (!utils.isBrowser) { - this.http = new bcoin.http.server({ + this.http = new HTTPServer({ network: this.network, logger: this.logger, node: this, @@ -122,20 +132,20 @@ SPVNode.prototype._init = function _init() { this.pool.on('tx', function(tx) { self.emit('tx', tx); - self.walletdb.addTX(tx, onError); + self.walletdb.addTX(tx).catch(onError); }); this.chain.on('block', function(block, entry) { self.emit('block', block); - self.walletdb.addBlock(entry, block.txs, onError); + self.walletdb.addBlock(entry, block.txs).catch(onError); }); - this.walletdb.on('save address', function(address, path) { - self.pool.watch(address.getHash()); + this.walletdb.on('path', function(path) { + self.pool.watch(path.hash, 'hex'); }); this.walletdb.on('send', function(tx) { - self.sendTX(tx, onError); + self.sendTX(tx).catch(onError); }); }; @@ -143,108 +153,88 @@ SPVNode.prototype._init = function _init() { * Open the node and all its child objects, * wait for the database to load. * @alias SPVNode#open - * @param {Function} callback + * @returns {Promise} */ -SPVNode.prototype._open = function open(callback) { - var self = this; +SPVNode.prototype._open = co(function* open(callback) { + yield this.chain.open(); + yield this.pool.open(); + yield this.walletdb.open(); - utils.serial([ - this.chain.open.bind(this.chain), - this.pool.open.bind(this.pool), - this.walletdb.open.bind(this.walletdb), - // Ensure primary wallet. - this.openWallet.bind(this), - // Load bloom filter. - this.openFilter.bind(this), - // Rescan for any missed transactions. - this.rescan.bind(this), - // Rebroadcast pending transactions. - this.resend.bind(this), - function(next) { - if (!self.http) - return next(); - self.http.open(next); - } - ], function(err) { - if (err) - return callback(err); + // Ensure primary wallet. + yield this.openWallet(); - self.logger.info('Node is loaded.'); + // Load bloom filter. + yield this.openFilter(); - callback(); - }); -}; + // Rescan for any missed transactions. + yield this.rescan(); + + // Rebroadcast pending transactions. + yield this.resend(); + + if (this.http) + yield this.http.open(); + + this.logger.info('Node is loaded.'); +}); /** * Close the node, wait for the database to close. * @alias SPVNode#close - * @param {Function} callback + * @returns {Promise} */ -SPVNode.prototype._close = function close(callback) { - var self = this; +SPVNode.prototype._close = co(function* close() { + if (this.http) + yield this.http.close(); + + yield this.wallet.destroy(); this.wallet = null; - utils.parallel([ - function(next) { - if (!self.http) - return next(); - self.http.close(next); - }, - this.walletdb.close.bind(this.walletdb), - this.pool.close.bind(this.pool), - this.chain.close.bind(this.chain) - ], callback); -}; + yield this.walletdb.close(); + yield this.pool.close(); + yield this.chain.close(); +}); /** * Initialize p2p bloom filter for address watching. - * @param {Function} callback + * @returns {Promise} */ -SPVNode.prototype.openFilter = function openFilter(callback) { - var self = this; +SPVNode.prototype.openFilter = co(function* openFilter() { + var hashes = yield this.walletdb.getAddressHashes(); var i; - this.walletdb.getAddressHashes(function(err, hashes) { - if (err) - return callback(err); + if (hashes.length > 0) + this.logger.info('Adding %d addresses to filter.', hashes.length); - if (hashes.length > 0) - self.logger.info('Adding %d addresses to filter.', hashes.length); - - for (i = 0; i < hashes.length; i++) - self.pool.watch(hashes[i], 'hex'); - - callback(); - }); -}; + for (i = 0; i < hashes.length; i++) + this.pool.watch(hashes[i], 'hex'); +}); /** * Rescan for any missed transactions. * Note that this will replay the blockchain sync. - * @param {Function} callback + * @returns {Promise} */ -SPVNode.prototype.rescan = function rescan(callback) { +SPVNode.prototype.rescan = function rescan() { if (this.options.noScan) { - this.walletdb.setTip( + return this.walletdb.setTip( this.chain.tip.hash, - this.chain.height, - callback); - return; + this.chain.height); } if (this.walletdb.height === 0) - return callback(); + return Promise.resolve(null); // Always replay the last block to make // sure we didn't miss anything: there // is no atomicity between the chaindb // and walletdb. - this.chain.reset(this.walletdb.height - 1, callback); + return this.chain.reset(this.walletdb.height - 1); }; /** @@ -252,11 +242,11 @@ SPVNode.prototype.rescan = function rescan(callback) { * by the mempool - use with care, lest you get banned from * bitcoind nodes). * @param {TX|Block} item - * @param {Function} callback + * @returns {Promise} */ -SPVNode.prototype.broadcast = function broadcast(item, callback) { - return this.pool.broadcast(item, callback); +SPVNode.prototype.broadcast = function broadcast(item) { + return this.pool.broadcast(item); }; /** @@ -264,21 +254,11 @@ SPVNode.prototype.broadcast = function broadcast(item, callback) { * by the mempool - use with care, lest you get banned from * bitcoind nodes). * @param {TX} tx - * @param {Function} callback + * @returns {Promise} */ -SPVNode.prototype.sendTX = function sendTX(tx, wait, callback) { - if (!callback) { - callback = wait; - wait = null; - } - - if (!wait) { - this.pool.broadcast(tx); - return utils.nextTick(callback); - } - - this.pool.broadcast(tx, callback); +SPVNode.prototype.sendTX = function sendTX(tx) { + return this.pool.broadcast(tx); }; /** diff --git a/lib/primitives/abstractblock.js b/lib/primitives/abstractblock.js index 38ac4a3f..dade5bef 100644 --- a/lib/primitives/abstractblock.js +++ b/lib/primitives/abstractblock.js @@ -7,12 +7,17 @@ 'use strict'; -var bcoin = require('../env'); -var constants = bcoin.constants; -var utils = bcoin.utils; +module.exports = AbstractBlock; + +var constants = require('../protocol/constants'); +var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; +var assert = require('assert'); var VerifyResult = utils.VerifyResult; +var BufferWriter = require('../utils/writer'); +var time = require('../net/timedata'); +var InvItem = require('./invitem'); +var Headers = require('./headers'); /** * The class which all block-like objects inherit from. @@ -151,9 +156,9 @@ AbstractBlock.prototype.hash = function hash(enc) { */ AbstractBlock.prototype.abbr = function abbr(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); - p.write32(this.version); + p.writeU32(this.version); p.writeHash(this.prevBlock); p.writeHash(this.merkleRoot); p.writeU32(this.ts); @@ -205,7 +210,7 @@ AbstractBlock.prototype.verifyHeaders = function verifyHeaders(ret) { } // Check timestamp against now + 2 hours - if (this.ts > bcoin.now() + 2 * 60 * 60) { + if (this.ts > time.now() + 2 * 60 * 60) { ret.reason = 'time-too-new'; ret.score = 0; return false; @@ -242,7 +247,7 @@ AbstractBlock.prototype.__defineGetter__('rhash', function() { */ AbstractBlock.prototype.toInv = function toInv() { - return new bcoin.invitem(constants.inv.BLOCK, this.hash('hex')); + return new InvItem(constants.inv.BLOCK, this.hash('hex')); }; /** @@ -251,7 +256,7 @@ AbstractBlock.prototype.toInv = function toInv() { */ AbstractBlock.prototype.toHeaders = function toHeaders() { - var headers = new bcoin.headers(this); + var headers = new Headers(this); headers._hash = this._hash; headers._valid = true; return headers; diff --git a/lib/primitives/address.js b/lib/primitives/address.js index 70ca0118..82d62ff1 100644 --- a/lib/primitives/address.js +++ b/lib/primitives/address.js @@ -7,16 +7,17 @@ 'use strict'; -var bcoin = require('../env'); -var networks = bcoin.networks; -var constants = bcoin.constants; +module.exports = Address; + +var Network = require('../protocol/network'); +var networks = require('../protocol/networks'); +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; +var assert = require('assert'); var BufferWriter = require('../utils/writer'); var BufferReader = require('../utils/reader'); -var Script = bcoin.script; -var scriptTypes = constants.scriptTypes; +var Script = require('../script/script'); /** * Represents an address. @@ -39,9 +40,9 @@ function Address(options) { return new Address(options); this.hash = constants.ZERO_HASH160; - this.type = scriptTypes.PUBKEYHASH; + this.type = Script.types.PUBKEYHASH; this.version = -1; - this.network = bcoin.network.get(); + this.network = Network.primary; if (options) this.fromOptions(options); @@ -113,7 +114,7 @@ Address.prototype.toRaw = function toRaw(network) { if (!network) network = this.network; - network = bcoin.network.get(network); + network = Network.get(network); prefix = Address.getPrefix(this.type, network); assert(prefix !== -1, 'Not a valid address prefix.'); @@ -258,42 +259,42 @@ Address.fromBase58 = function fromBase58(address) { Address.prototype.fromScript = function fromScript(script) { if (script.isPubkey()) { this.hash = crypto.hash160(script.get(0)); - this.type = scriptTypes.PUBKEYHASH; + this.type = Script.types.PUBKEYHASH; this.version = -1; return this; } if (script.isPubkeyhash()) { this.hash = script.get(2); - this.type = scriptTypes.PUBKEYHASH; + this.type = Script.types.PUBKEYHASH; this.version = -1; return this; } if (script.isScripthash()) { this.hash = script.get(1); - this.type = scriptTypes.SCRIPTHASH; + this.type = Script.types.SCRIPTHASH; this.version = -1; return this; } if (script.isWitnessPubkeyhash()) { this.hash = script.get(1); - this.type = scriptTypes.WITNESSPUBKEYHASH; + this.type = Script.types.WITNESSPUBKEYHASH; this.version = 0; return this; } if (script.isWitnessScripthash()) { this.hash = script.get(1); - this.type = scriptTypes.WITNESSSCRIPTHASH; + this.type = Script.types.WITNESSSCRIPTHASH; this.version = 0; return this; } if (script.isWitnessMasthash()) { this.hash = script.get(1); - this.type = scriptTypes.WITNESSSCRIPTHASH; + this.type = Script.types.WITNESSSCRIPTHASH; this.version = 1; return this; } @@ -301,7 +302,7 @@ Address.prototype.fromScript = function fromScript(script) { // Put this last: it's the slowest to check. if (script.isMultisig()) { this.hash = script.hash160(); - this.type = scriptTypes.SCRIPTHASH; + this.type = Script.types.SCRIPTHASH; this.version = -1; return this; } @@ -318,14 +319,14 @@ Address.prototype.fromWitness = function fromWitness(witness) { // since we can't get the version. if (witness.isPubkeyhashInput()) { this.hash = crypto.hash160(witness.get(1)); - this.type = scriptTypes.WITNESSPUBKEYHASH; + this.type = Script.types.WITNESSPUBKEYHASH; this.version = 0; return this; } if (witness.isScripthashInput()) { this.hash = crypto.sha256(witness.get(witness.length - 1)); - this.type = scriptTypes.WITNESSSCRIPTHASH; + this.type = Script.types.WITNESSSCRIPTHASH; this.version = 0; return this; } @@ -340,14 +341,14 @@ Address.prototype.fromWitness = function fromWitness(witness) { Address.prototype.fromInputScript = function fromInputScript(script) { if (script.isPubkeyhashInput()) { this.hash = crypto.hash160(script.get(1)); - this.type = scriptTypes.PUBKEYHASH; + this.type = Script.types.PUBKEYHASH; this.version = -1; return this; } if (script.isScripthashInput()) { this.hash = crypto.hash160(script.get(script.length - 1)); - this.type = scriptTypes.SCRIPTHASH; + this.type = Script.types.SCRIPTHASH; this.version = -1; return this; } @@ -405,15 +406,15 @@ Address.prototype.fromHash = function fromHash(hash, type, version, network) { hash = new Buffer(hash, 'hex'); if (typeof type === 'string') - type = scriptTypes[type.toUpperCase()]; + type = Script.types[type.toUpperCase()]; if (type == null) - type = scriptTypes.PUBKEYHASH; + type = Script.types.PUBKEYHASH; if (version == null) version = -1; - network = bcoin.network.get(network); + network = Network.get(network); assert(Buffer.isBuffer(hash)); assert(utils.isNumber(type)); @@ -427,11 +428,11 @@ Address.prototype.fromHash = function fromHash(hash, type, version, network) { } else { assert(Address.isWitness(type), 'Wrong version (non-witness).'); assert(version >= 0 && version <= 16, 'Bad program version.'); - if (version === 0 && type === scriptTypes.WITNESSPUBKEYHASH) + if (version === 0 && type === Script.types.WITNESSPUBKEYHASH) assert(hash.length === 20, 'Hash is the wrong size.'); - else if (version === 0 && type === scriptTypes.WITNESSSCRIPTHASH) + else if (version === 0 && type === Script.types.WITNESSSCRIPTHASH) assert(hash.length === 32, 'Hash is the wrong size.'); - else if (version === 1 && type === scriptTypes.WITNESSSCRIPTHASH) + else if (version === 1 && type === Script.types.WITNESSSCRIPTHASH) assert(hash.length === 32, 'Hash is the wrong size.'); } @@ -468,9 +469,9 @@ Address.fromHash = function fromHash(hash, type, version, network) { Address.prototype.fromData = function fromData(data, type, version, network) { if (typeof type === 'string') - type = scriptTypes[type.toUpperCase()]; + type = Script.types[type.toUpperCase()]; - if (type === scriptTypes.WITNESSSCRIPTHASH) { + if (type === Script.types.WITNESSSCRIPTHASH) { if (version === 0) { assert(Buffer.isBuffer(data)); data = crypto.sha256(data); @@ -480,7 +481,7 @@ Address.prototype.fromData = function fromData(data, type, version, network) { } else { throw new Error('Cannot create from version=' + version); } - } else if (type === scriptTypes.WITNESSPUBKEYHASH) { + } else if (type === Script.types.WITNESSPUBKEYHASH) { if (version !== 0) throw new Error('Cannot create from version=' + version); assert(Buffer.isBuffer(data)); @@ -529,7 +530,7 @@ Address.validate = function validate(address, type) { } if (typeof type === 'string') - type = scriptTypes[type.toUpperCase()]; + type = Script.types[type.toUpperCase()]; if (type && address.type !== type) return false; @@ -579,13 +580,13 @@ Address.getHash = function getHash(data, enc) { Address.getPrefix = function getPrefix(type, network) { var prefixes = network.addressPrefix; switch (type) { - case scriptTypes.PUBKEYHASH: + case Script.types.PUBKEYHASH: return prefixes.pubkeyhash; - case scriptTypes.SCRIPTHASH: + case Script.types.SCRIPTHASH: return prefixes.scripthash; - case scriptTypes.WITNESSPUBKEYHASH: + case Script.types.WITNESSPUBKEYHASH: return prefixes.witnesspubkeyhash; - case scriptTypes.WITNESSSCRIPTHASH: + case Script.types.WITNESSSCRIPTHASH: return prefixes.witnessscripthash; default: return -1; @@ -603,13 +604,13 @@ Address.getType = function getType(prefix, network) { var prefixes = network.addressPrefix; switch (prefix) { case prefixes.pubkeyhash: - return scriptTypes.PUBKEYHASH; + return Script.types.PUBKEYHASH; case prefixes.scripthash: - return scriptTypes.SCRIPTHASH; + return Script.types.SCRIPTHASH; case prefixes.witnesspubkeyhash: - return scriptTypes.WITNESSPUBKEYHASH; + return Script.types.WITNESSPUBKEYHASH; case prefixes.witnessscripthash: - return scriptTypes.WITNESSSCRIPTHASH; + return Script.types.WITNESSSCRIPTHASH; default: return -1; } @@ -623,9 +624,9 @@ Address.getType = function getType(prefix, network) { Address.isWitness = function isWitness(type) { switch (type) { - case scriptTypes.WITNESSPUBKEYHASH: + case Script.types.WITNESSPUBKEYHASH: return true; - case scriptTypes.WITNESSSCRIPTHASH: + case Script.types.WITNESSSCRIPTHASH: return true; default: return false; diff --git a/lib/primitives/block.js b/lib/primitives/block.js index 2b51622d..26d60f9b 100644 --- a/lib/primitives/block.js +++ b/lib/primitives/block.js @@ -7,13 +7,19 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Block; + var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; -var AbstractBlock = bcoin.abstractblock; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var AbstractBlock = require('./abstractblock'); var VerifyResult = utils.VerifyResult; +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); +var TX = require('./tx'); +var MerkleBlock = require('./merkleblock'); +var Network = require('../protocol/network'); /** * Represents a full block. @@ -162,7 +168,7 @@ Block.prototype.getSizes = function getSizes() { }; } - writer = new bcoin.writer(); + writer = new BufferWriter(); this.toRaw(writer); return { @@ -262,7 +268,7 @@ Block.prototype.hasTX = function hasTX(hash) { Block.prototype.indexOf = function indexOf(hash) { var i; - if (hash instanceof bcoin.tx) + if (hash instanceof TX) hash = hash.hash('hex'); for (i = 0; i < this.txs.length; i++) { @@ -534,7 +540,7 @@ Block.reward = function reward(height, network) { assert(height >= 0, 'Bad height for reward.'); - network = bcoin.network.get(network); + network = Network.get(network); halvings = height / network.halvingInterval | 0; // BIP 42 (well, our own version of it, @@ -647,7 +653,7 @@ Block.prototype.fromJSON = function fromJSON(json) { this.parseJSON(json); for (i = 0; i < json.txs.length; i++) - this.txs.push(bcoin.tx.fromJSON(json.txs[i])); + this.txs.push(TX.fromJSON(json.txs[i])); return this; }; @@ -669,7 +675,7 @@ Block.fromJSON = function fromJSON(json) { */ Block.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, tx, witnessSize; p.start(); @@ -685,7 +691,7 @@ Block.prototype.fromRaw = function fromRaw(data) { witnessSize = 0; for (i = 0; i < this.totalTX; i++) { - tx = bcoin.tx.fromRaw(p); + tx = TX.fromRaw(p); witnessSize += tx._witnessSize; this.addTX(tx); } @@ -721,7 +727,7 @@ Block.fromRaw = function fromRaw(data, enc) { */ Block.prototype.toMerkle = function toMerkle(filter) { - return bcoin.merkleblock.fromBlock(this, filter); + return MerkleBlock.fromBlock(this, filter); }; /** @@ -733,11 +739,11 @@ Block.prototype.toMerkle = function toMerkle(filter) { */ Block.prototype.frame = function frame(witness, writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var witnessSize = 0; var i, tx; - p.write32(this.version); + p.writeU32(this.version); p.writeHash(this.prevBlock); p.writeHash(this.merkleRoot); p.writeU32(this.ts); diff --git a/lib/primitives/coin.js b/lib/primitives/coin.js index 92bf2ffe..af40a9bd 100644 --- a/lib/primitives/coin.js +++ b/lib/primitives/coin.js @@ -7,11 +7,20 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Coin; + var utils = require('../utils/utils'); -var constants = bcoin.constants; -var assert = utils.assert; -var Output = bcoin.output; +var constants = require('../protocol/constants'); +var Network = require('../protocol/network'); +var assert = require('assert'); +var Output = require('./output'); +var Script = require('../script/script'); +var Network = require('../protocol/network'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); +var compressor = require('../chain/compress'); +var compress = compressor.compress; +var decompress = compressor.decompress; /** * Represents an unspent output. @@ -36,7 +45,7 @@ function Coin(options) { this.version = 1; this.height = -1; this.value = 0; - this.script = new bcoin.script(); + this.script = new Script(); this.coinbase = true; this.hash = constants.NULL_HASH; this.index = 0; @@ -95,7 +104,7 @@ Coin.fromOptions = function fromOptions(options) { Coin.prototype.getConfirmations = function getConfirmations(height) { if (height == null) - height = bcoin.network.get().height; + height = Network.primary.height; if (this.height === -1) return 0; @@ -210,7 +219,7 @@ Coin.prototype.fromJSON = function fromJSON(json) { */ Coin.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var height = this.height; if (height === -1) @@ -235,7 +244,7 @@ Coin.prototype.toRaw = function toRaw(writer) { */ Coin.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.version = p.readU32(); this.height = p.readU32(); @@ -269,8 +278,7 @@ Coin.fromRaw = function fromRaw(data, enc) { */ Coin.prototype.toCompressed = function toCompressed(writer) { - var compress = bcoin.coins.compress; - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var height = this.height; var bits; @@ -303,8 +311,7 @@ Coin.prototype.toCompressed = function toCompressed(writer) { */ Coin.prototype.fromCompressed = function fromCompressed(data) { - var decompress = bcoin.coins.decompress; - var p = bcoin.reader(data); + var p = BufferReader(data); var bits; this.version = p.readVarint(); @@ -341,7 +348,7 @@ Coin.fromCompressed = function fromCompressed(data, enc) { */ Coin.prototype.toExtended = function toExtended(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); this.toRaw(p); p.writeHash(this.hash); @@ -360,7 +367,7 @@ Coin.prototype.toExtended = function toExtended(writer) { */ Coin.prototype.fromExtended = function fromExtended(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.fromRaw(p); this.hash = p.readHash('hex'); this.index = p.readU32(); diff --git a/lib/primitives/headers.js b/lib/primitives/headers.js index bffd2dc4..e15bbdd2 100644 --- a/lib/primitives/headers.js +++ b/lib/primitives/headers.js @@ -7,9 +7,12 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Headers; + var utils = require('../utils/utils'); -var AbstractBlock = bcoin.abstractblock; +var AbstractBlock = require('./abstractblock'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); /** * Represents block headers obtained from the network via `headers`. @@ -46,7 +49,7 @@ Headers.prototype._verify = function _verify(ret) { */ Headers.prototype.getSize = function getSize() { - var writer = new bcoin.writer(); + var writer = new BufferWriter(); this.toRaw(writer); return writer.written; }; @@ -80,9 +83,9 @@ Headers.prototype.inspect = function inspect() { */ Headers.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); - p.write32(this.version); + p.writeU32(this.version); p.writeHash(this.prevBlock); p.writeHash(this.merkleRoot); p.writeU32(this.ts); @@ -103,7 +106,7 @@ Headers.prototype.toRaw = function toRaw(writer) { */ Headers.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.version = p.readU32(); // Technically signed this.prevBlock = p.readHash('hex'); @@ -136,7 +139,7 @@ Headers.fromRaw = function fromRaw(data, enc) { */ Headers.prototype.fromAbbr = function fromAbbr(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.version = p.readU32(); // Technically signed this.prevBlock = p.readHash('hex'); diff --git a/lib/primitives/index.js b/lib/primitives/index.js new file mode 100644 index 00000000..f67150d8 --- /dev/null +++ b/lib/primitives/index.js @@ -0,0 +1,17 @@ +'use strict'; + +exports.AbstractBlock = require('./abstractblock'); +exports.Address = require('./address'); +exports.Block = require('./block'); +exports.Coin = require('./coin'); +exports.Headers = require('./headers'); +exports.Input = require('./input'); +exports.InvItem = require('./invitem'); +exports.KeyRing = require('./keyring'); +exports.MemBlock = require('./memblock'); +exports.MerkleBlock = require('./merkleblock'); +exports.MTX = require('./mtx'); +exports.NetworkAddress = require('./netaddress'); +exports.Outpoint = require('./outpoint'); +exports.Output = require('./output'); +exports.TX = require('./tx'); diff --git a/lib/primitives/input.js b/lib/primitives/input.js index a7ca2f52..1f8b3c9a 100644 --- a/lib/primitives/input.js +++ b/lib/primitives/input.js @@ -7,11 +7,17 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Input; + var utils = require('../utils/utils'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var Script = require('../script/script'); +var Witness = require('../script/witness'); var Outpoint = require('./outpoint'); +var Coin = require('./coin'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); /** * Represents a transaction input. @@ -30,9 +36,9 @@ function Input(options) { return new Input(options); this.prevout = new Outpoint(); - this.script = new bcoin.script(); + this.script = new Script(); this.sequence = 0xffffffff; - this.witness = new bcoin.witness(); + this.witness = new Witness(); this.coin = null; this.mutable = false; this._address = null; @@ -65,7 +71,7 @@ Input.prototype.fromOptions = function fromOptions(options) { this.witness.fromOptions(options.witness); if (options.coin) - this.coin = bcoin.coin(options.coin); + this.coin = Coin(options.coin); return this; }; @@ -277,7 +283,7 @@ Input.prototype.fromJSON = function fromJSON(json) { assert(json, 'Input data is required.'); assert(utils.isNumber(json.sequence)); this.prevout.fromJSON(json.prevout); - this.coin = json.coin ? bcoin.coin.fromJSON(json.coin) : null; + this.coin = json.coin ? Coin.fromJSON(json.coin) : null; this.script.fromJSON(json.script); this.witness.fromJSON(json.witness); this.sequence = json.sequence; @@ -301,7 +307,7 @@ Input.fromJSON = function fromJSON(json) { */ Input.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); this.prevout.toRaw(p); p.writeVarBytes(this.script.toRaw()); @@ -319,7 +325,7 @@ Input.prototype.toRaw = function toRaw(writer) { */ Input.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.prevout.fromRaw(p); this.script.fromRaw(p.readVarBytes()); @@ -349,7 +355,7 @@ Input.fromRaw = function fromRaw(data, enc) { */ Input.prototype.toExtended = function toExtended(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); this.toRaw(p); this.witness.toRaw(p); @@ -367,7 +373,7 @@ Input.prototype.toExtended = function toExtended(writer) { */ Input.prototype.fromExtended = function fromExtended(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.fromRaw(p); this.witness.fromRaw(p); return this; @@ -420,7 +426,7 @@ Input.fromCoin = function fromCoin(coin) { */ Input.prototype.fromTX = function fromTX(tx, index) { - var coin = bcoin.coin.fromTX(tx, index); + var coin = Coin.fromTX(tx, index); return this.fromCoin(coin); }; diff --git a/lib/primitives/invitem.js b/lib/primitives/invitem.js index 4f8da655..204e86dc 100644 --- a/lib/primitives/invitem.js +++ b/lib/primitives/invitem.js @@ -7,8 +7,11 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = InvItem; + var constants = require('../protocol/constants'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); /** * Inv Item @@ -32,7 +35,7 @@ function InvItem(type, hash) { */ InvItem.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeU32(this.type); p.writeHash(this.hash); @@ -49,7 +52,7 @@ InvItem.prototype.toRaw = function toRaw(writer) { */ InvItem.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.type = p.readU32(); this.hash = p.readHash('hex'); return this; diff --git a/lib/primitives/keyring.js b/lib/primitives/keyring.js index 903ebabd..aac6f3b2 100644 --- a/lib/primitives/keyring.js +++ b/lib/primitives/keyring.js @@ -7,44 +7,46 @@ 'use strict'; -var bcoin = require('../env'); -var constants = bcoin.constants; -var utils = bcoin.utils; +module.exports = KeyRing; + +var constants = require('../protocol/constants'); +var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var networks = bcoin.networks; +var assert = require('assert'); +var networks = require('../protocol/networks'); +var Network = require('../protocol/network'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); -var scriptTypes = constants.scriptTypes; +var Script = require('../script/script'); +var Address = require('./address'); +var Input = require('./input'); +var Output = require('./output'); +var ec = require('../crypto/ec'); /** * Represents a key ring which amounts to an address. * @exports KeyRing * @constructor * @param {Object} options - * @param {HDPrivateKey|HDPublicKey|Buffer} options.key - * @param {Buffer[]} options.keys - Shared multisig keys. - * @param {Number?} options.m - Multisig `m` value. - * @param {Number?} options.n - Multisig `n` value. - * @param {Boolean?} options.witness - Whether witness programs are enabled. + * @param {Network} network */ function KeyRing(options, network) { if (!(this instanceof KeyRing)) return new KeyRing(options, network); - this.network = bcoin.network.get(); + this.network = Network.primary; this.witness = false; + this.nested = false; this.publicKey = constants.ZERO_KEY; this.privateKey = null; this.script = null; - this.path = null; this._keyHash = null; this._keyAddress = null; this._program = null; - this._programHash = null; - this._programAddress = null; + this._nestedHash = null; + this._nestedAddress = null; this._scriptHash160 = null; this._scriptHash256 = null; this._scriptAddress = null; @@ -61,6 +63,11 @@ function KeyRing(options, network) { KeyRing.prototype.fromOptions = function fromOptions(options, network) { var key = toKey(options); + var script = options.script; + var compressed = options.compressed; + + if (!network) + network = options.network; if (Buffer.isBuffer(key)) return this.fromKey(key, network); @@ -73,18 +80,20 @@ KeyRing.prototype.fromOptions = function fromOptions(options, network) { if (options.privateKey) key = toKey(options.privateKey); - if (options.network) - this.network = bcoin.network.get(options.network); - if (options.witness != null) { assert(typeof options.witness === 'boolean'); this.witness = options.witness; } - if (options.script) - return this.fromScript(key, options.script, this.network); + if (options.nested != null) { + assert(typeof options.nested === 'boolean'); + this.nested = options.nested; + } - this.fromKey(key, this.network); + if (script) + return this.fromScript(key, script, compressed, network); + + this.fromKey(key, compressed, network); }; /** @@ -100,59 +109,82 @@ KeyRing.fromOptions = function fromOptions(options) { /** * Inject data from private key. * @private - * @param {Buffer} privateKey + * @param {Buffer} key * @param {Boolean?} compressed * @param {(NetworkType|Network}) network */ -KeyRing.prototype.fromPrivate = function fromPrivate(privateKey, network) { - assert(Buffer.isBuffer(privateKey), 'Private key must be a buffer.'); - assert(bcoin.ec.privateKeyVerify(privateKey), 'Not a valid private key.'); - this.network = bcoin.network.get(network); - this.privateKey = privateKey; - this.publicKey = bcoin.ec.publicKeyCreate(this.privateKey, true); +KeyRing.prototype.fromPrivate = function fromPrivate(key, compressed, network) { + assert(Buffer.isBuffer(key), 'Private key must be a buffer.'); + assert(ec.privateKeyVerify(key), 'Not a valid private key.'); + + if (typeof compressed !== 'boolean') { + network = compressed; + compressed = null; + } + + this.network = Network.get(network); + this.privateKey = key; + this.publicKey = ec.publicKeyCreate(key, compressed !== false); + return this; }; /** * Instantiate keyring from a private key. - * @param {Buffer} privateKey + * @param {Buffer} key * @param {Boolean?} compressed * @param {(NetworkType|Network}) network * @returns {KeyRing} */ -KeyRing.fromPrivate = function fromPrivate(privateKey, network) { - return new KeyRing().fromPrivate(privateKey, network); +KeyRing.fromPrivate = function fromPrivate(key, compressed, network) { + return new KeyRing().fromPrivate(key, compressed, network); }; /** * Inject data from public key. * @private - * @param {Buffer} privateKey + * @param {Buffer} key * @param {(NetworkType|Network}) network */ -KeyRing.prototype.fromPublic = function fromPublic(publicKey, network) { - assert(Buffer.isBuffer(publicKey), 'Public key must be a buffer.'); - assert(bcoin.ec.publicKeyVerify(publicKey), 'Not a valid public key.'); - this.network = bcoin.network.get(network); - this.publicKey = publicKey; +KeyRing.prototype.fromPublic = function fromPublic(key, network) { + assert(Buffer.isBuffer(key), 'Public key must be a buffer.'); + assert(ec.publicKeyVerify(key), 'Not a valid public key.'); + this.network = Network.get(network); + this.publicKey = key; return this; }; +/** + * Generate a keyring. + * @private + * @param {(Network|NetworkType)?} network + * @returns {KeyRing} + */ + +KeyRing.prototype.generate = function(compressed, network) { + var key; + + if (typeof compressed !== 'boolean') { + network = compressed; + compressed = null; + } + + key = ec.generatePrivateKey(); + + return this.fromKey(key, compressed, network); +}; + /** * Generate a keyring. * @param {(Network|NetworkType)?} network * @returns {KeyRing} */ -KeyRing.generate = function(network) { - var key = new KeyRing(); - key.network = bcoin.network.get(network); - key.privateKey = bcoin.ec.generatePrivateKey(); - key.publicKey = bcoin.ec.publicKeyCreate(key.privateKey, true); - return key; +KeyRing.generate = function(compressed, network) { + return new KeyRing().generate(compressed, network); }; /** @@ -162,8 +194,8 @@ KeyRing.generate = function(network) { * @returns {KeyRing} */ -KeyRing.fromPublic = function fromPublic(publicKey, network) { - return new KeyRing().fromPublic(publicKey, network); +KeyRing.fromPublic = function fromPublic(key, network) { + return new KeyRing().fromPublic(key, network); }; /** @@ -173,14 +205,18 @@ KeyRing.fromPublic = function fromPublic(publicKey, network) { * @param {(NetworkType|Network}) network */ -KeyRing.prototype.fromKey = function fromKey(key, network) { +KeyRing.prototype.fromKey = function fromKey(key, compressed, network) { assert(Buffer.isBuffer(key), 'Key must be a buffer.'); - assert(key.length === 32 || key.length === 33, 'Not a key.'); - if (key.length === 33) - return this.fromPublic(key, network); + if (typeof compressed !== 'boolean') { + network = compressed; + compressed = null; + } - return this.fromPrivate(key, network); + if (key.length === 32) + return this.fromPrivate(key, compressed !== false, network); + + return this.fromPublic(key, network); }; /** @@ -190,8 +226,8 @@ KeyRing.prototype.fromKey = function fromKey(key, network) { * @returns {KeyRing} */ -KeyRing.fromKey = function fromKey(key, network) { - return new KeyRing().fromKey(key, network); +KeyRing.fromKey = function fromKey(key, compressed, network) { + return new KeyRing().fromKey(key, compressed, network); }; /** @@ -202,10 +238,17 @@ KeyRing.fromKey = function fromKey(key, network) { * @param {(NetworkType|Network}) network */ -KeyRing.prototype.fromScript = function fromScript(key, script, network) { - assert(script instanceof bcoin.script, 'Non-script passed into KeyRing.'); - this.fromKey(key, network); +KeyRing.prototype.fromScript = function fromScript(key, script, compressed, network) { + assert(script instanceof Script, 'Non-script passed into KeyRing.'); + + if (typeof compressed !== 'boolean') { + network = compressed; + compressed = null; + } + + this.fromKey(key, compressed, network); this.script = script; + return this; }; @@ -217,8 +260,8 @@ KeyRing.prototype.fromScript = function fromScript(key, script, network) { * @returns {KeyRing} */ -KeyRing.fromScript = function fromScript(key, script, network) { - return new KeyRing().fromScript(key, script, network); +KeyRing.fromScript = function fromScript(key, script, compressed, network) { + return new KeyRing().fromScript(key, script, compressed, network); }; /** @@ -235,7 +278,8 @@ KeyRing.prototype.toSecret = function toSecret() { p.writeU8(this.network.keyPrefix.privkey); p.writeBytes(this.privateKey); - p.writeU8(1); + if (this.publicKey.length === 33) + p.writeU8(1); p.writeChecksum(); @@ -274,9 +318,7 @@ KeyRing.prototype.fromSecret = function fromSecret(data) { p.verifyChecksum(); - assert(compressed === false, 'Cannot handle uncompressed.'); - - return this.fromPrivate(key, type); + return this.fromPrivate(key, compressed, type); }; /** @@ -347,10 +389,10 @@ KeyRing.prototype.getProgram = function getProgram() { if (!this._program) { if (!this.script) { hash = crypto.hash160(this.publicKey); - program = bcoin.script.fromProgram(0, hash); + program = Script.fromProgram(0, hash); } else { hash = this.script.sha256(); - program = bcoin.script.fromProgram(0, hash); + program = Script.fromProgram(0, hash); } this._program = program; } @@ -365,16 +407,16 @@ KeyRing.prototype.getProgram = function getProgram() { * @returns {Buffer} */ -KeyRing.prototype.getProgramHash = function getProgramHash(enc) { +KeyRing.prototype.getNestedHash = function getNestedHash(enc) { if (!this.witness) return; - if (!this._programHash) - this._programHash = this.getProgram().hash160(); + if (!this._nestedHash) + this._nestedHash = this.getProgram().hash160(); return enc === 'hex' - ? this._programHash.toString('hex') - : this._programHash; + ? this._nestedHash.toString('hex') + : this._nestedHash; }; /** @@ -383,22 +425,22 @@ KeyRing.prototype.getProgramHash = function getProgramHash(enc) { * @returns {Address|Base58Address} */ -KeyRing.prototype.getProgramAddress = function getProgramAddress(enc) { +KeyRing.prototype.getNestedAddress = function getNestedAddress(enc) { var hash, address; if (!this.witness) return; - if (!this._programAddress) { - hash = this.getProgramHash(); - address = this.compile(hash, scriptTypes.SCRIPTHASH); - this._programAddress = address; + if (!this._nestedAddress) { + hash = this.getNestedHash(); + address = this.compile(hash, Script.types.SCRIPTHASH); + this._nestedAddress = address; } if (enc === 'base58') - return this._programAddress.toBase58(); + return this._nestedAddress.toBase58(); - return this._programAddress; + return this._nestedAddress; }; /** @@ -464,10 +506,10 @@ KeyRing.prototype.getScriptAddress = function getScriptAddress(enc) { if (!this._scriptAddress) { if (this.witness) { hash = this.getScriptHash256(); - address = this.compile(hash, scriptTypes.WITNESSSCRIPTHASH, 0); + address = this.compile(hash, Script.types.WITNESSSCRIPTHASH, 0); } else { hash = this.getScriptHash160(); - address = this.compile(hash, scriptTypes.SCRIPTHASH); + address = this.compile(hash, Script.types.SCRIPTHASH); } this._scriptAddress = address; } @@ -505,9 +547,9 @@ KeyRing.prototype.getKeyAddress = function getKeyAddress(enc) { if (!this._keyAddress) { hash = this.getKeyHash(); if (this.witness) - address = this.compile(hash, scriptTypes.WITNESSPUBKEYHASH, 0); + address = this.compile(hash, Script.types.WITNESSPUBKEYHASH, 0); else - address = this.compile(hash, scriptTypes.PUBKEYHASH); + address = this.compile(hash, Script.types.PUBKEYHASH); this._keyAddress = address; } @@ -528,7 +570,7 @@ KeyRing.prototype.getKeyAddress = function getKeyAddress(enc) { */ KeyRing.prototype.compile = function compile(hash, type, version) { - return bcoin.address.fromHash(hash, type, version, this.network); + return Address.fromHash(hash, type, version, this.network); }; /** @@ -538,6 +580,8 @@ KeyRing.prototype.compile = function compile(hash, type, version) { */ KeyRing.prototype.getHash = function getHash(enc) { + if (this.nested) + return this.getNestedHash(enc); if (this.script) return this.getScriptHash(enc); return this.getKeyHash(enc); @@ -550,6 +594,8 @@ KeyRing.prototype.getHash = function getHash(enc) { */ KeyRing.prototype.getAddress = function getAddress(enc) { + if (this.nested) + return this.getNestedAddress(enc); if (this.script) return this.getScriptAddress(enc); return this.getKeyAddress(enc); @@ -572,7 +618,7 @@ KeyRing.prototype.ownHash = function ownHash(hash) { return true; if (this.witness) { - if (utils.equal(hash, this.programHash)) + if (utils.equal(hash, this.nestedHash)) return true; } @@ -589,7 +635,7 @@ KeyRing.prototype.ownHash = function ownHash(hash) { KeyRing.prototype.ownInput = function ownInput(tx, index) { var input; - if (tx instanceof bcoin.input) { + if (tx instanceof Input) { input = tx; } else { input = tx.inputs[index]; @@ -609,7 +655,7 @@ KeyRing.prototype.ownInput = function ownInput(tx, index) { KeyRing.prototype.ownOutput = function ownOutput(tx, index) { var output; - if (tx instanceof bcoin.output) { + if (tx instanceof Output) { output = tx; } else { output = tx.outputs[index]; @@ -628,7 +674,7 @@ KeyRing.prototype.ownOutput = function ownOutput(tx, index) { KeyRing.prototype.getRedeem = function(hash) { if (this.program) { - if (utils.equal(hash, this.programHash)) + if (utils.equal(hash, this.nestedHash)) return this.program; } @@ -651,7 +697,7 @@ KeyRing.prototype.getRedeem = function(hash) { KeyRing.prototype.sign = function sign(msg) { assert(this.privateKey, 'Cannot sign without private key.'); - return bcoin.ec.sign(msg, this.privateKey); + return ec.sign(msg, this.privateKey); }; /** @@ -662,7 +708,7 @@ KeyRing.prototype.sign = function sign(msg) { */ KeyRing.prototype.verify = function verify(msg, sig) { - return bcoin.ec.verify(msg, sig, this.publicKey); + return ec.verify(msg, sig, this.publicKey); }; /** @@ -670,14 +716,35 @@ KeyRing.prototype.verify = function verify(msg, sig) { * @returns {ScriptType} */ +KeyRing.prototype.getVersion = function getVersion() { + if (!this.witness) + return -1; + + if (this.nested) + return -1; + + return 0; +}; + +/** + * Get address type. + * @returns {ScriptType} + */ + KeyRing.prototype.getType = function getType() { - if (this.program) - return this.program.getType(); + if (this.nested) + return Script.types.SCRIPTHASH; + + if (this.witness) { + if (this.script) + return Script.types.WITNESSSCRIPTHASH; + return Script.types.WITNESSPUBKEYHASH; + } if (this.script) - return this.script.getType(); + return Script.types.SCRIPTHASH; - return scriptTypes.PUBKEYHASH; + return Script.types.PUBKEYHASH; }; /* @@ -688,6 +755,10 @@ KeyRing.prototype.__defineGetter__('type', function() { return this.getType(); }); +KeyRing.prototype.__defineGetter__('version', function() { + return this.getVersion(); +}); + KeyRing.prototype.__defineGetter__('scriptHash', function() { return this.getScriptHash(); }); @@ -708,12 +779,12 @@ KeyRing.prototype.__defineGetter__('program', function() { return this.getProgram(); }); -KeyRing.prototype.__defineGetter__('programHash', function() { - return this.getProgramHash(); +KeyRing.prototype.__defineGetter__('nestedHash', function() { + return this.getNestedHash(); }); -KeyRing.prototype.__defineGetter__('programAddress', function() { - return this.getProgramAddress(); +KeyRing.prototype.__defineGetter__('nestedAddress', function() { + return this.getNestedAddress(); }); KeyRing.prototype.__defineGetter__('keyHash', function() { @@ -750,17 +821,12 @@ KeyRing.prototype.toJSON = function toJSON() { return { network: this.network.type, witness: this.witness, + nested: this.nested, publicKey: this.publicKey.toString('hex'), script: this.script ? this.script.toRaw().toString('hex') : null, + program: this.program ? this.program.toRaw().toString('hex') : null, type: constants.scriptTypesByVal[this.type].toLowerCase(), - wid: this.path ? this.path.wid : undefined, - id: this.path ? this.path.id : undefined, - name: this.path ? this.path.name : undefined, - account: this.path ? this.path.account : undefined, - change: this.path ? this.path.change : undefined, - index: this.path ? this.path.index : undefined, - address: this.getAddress('base58'), - programAddress: this.getProgramAddress('base58') + address: this.getAddress('base58') }; }; @@ -774,11 +840,13 @@ KeyRing.prototype.fromJSON = function fromJSON(json) { assert(json); assert(typeof json.network === 'string'); assert(typeof json.witness === 'boolean'); + assert(typeof json.nested === 'boolean'); assert(typeof json.publicKey === 'string'); assert(!json.script || typeof json.script === 'string'); - this.nework = bcoin.network.get(json.network); + this.nework = Network.get(json.network); this.witness = json.witness; + this.nested = json.nested; this.publicKey = new Buffer(json.publicKey, 'hex'); if (json.script) @@ -804,13 +872,22 @@ KeyRing.fromJSON = function fromJSON(json) { KeyRing.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); + var field = 0; - p.writeU8(this.witness ? 1 : 0); + if (this.witness) + field |= 1; - if (this.privateKey) + if (this.nested) + field |= 2; + + p.writeU8(field); + + if (this.privateKey) { p.writeVarBytes(this.privateKey); - else + p.writeU8(this.publicKey.length === 33); + } else { p.writeVarBytes(this.publicKey); + } if (this.script) p.writeVarBytes(this.script.toRaw()); @@ -831,24 +908,30 @@ KeyRing.prototype.toRaw = function toRaw(writer) { KeyRing.prototype.fromRaw = function fromRaw(data, network) { var p = new BufferReader(data); - var key, script; + var field, compressed, key, script; - this.network = bcoin.network.get(network); - this.witness = p.readU8() === 1; + this.network = Network.get(network); + + field = p.readU8(); + + this.witness = (field & 1) !== 0; + this.nested = (field & 2) !== 0; key = p.readVarBytes(); if (key.length === 32) { + compressed = p.readU8() === 1; this.privateKey = key; - this.publicKey = bcoin.ec.publicKeyCreate(key, true); + this.publicKey = ec.publicKeyCreate(key, compressed); } else { this.publicKey = key; + assert(ec.publicKeyVerify(key), 'Invalid public key.'); } script = p.readVarBytes(); if (script.length > 0) - this.script = bcoin.script.fromRaw(script); + this.script = Script.fromRaw(script); return this; }; diff --git a/lib/primitives/memblock.js b/lib/primitives/memblock.js index 6f303cc8..2a95b708 100644 --- a/lib/primitives/memblock.js +++ b/lib/primitives/memblock.js @@ -7,9 +7,13 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = MemBlock; + var utils = require('../utils/utils'); -var AbstractBlock = bcoin.abstractblock; +var AbstractBlock = require('./abstractblock'); +var Block = require('./block'); +var Script = require('../script/script'); +var BufferReader = require('../utils/reader'); /** * A block object which is essentially a "placeholder" @@ -113,7 +117,7 @@ MemBlock.prototype.getCoinbaseHeight = function getCoinbaseHeight() { */ MemBlock.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data, true); + var p = BufferReader(data, true); var height = -1; var inCount, input; @@ -137,7 +141,7 @@ MemBlock.prototype.fromRaw = function fromRaw(data) { if (inCount > 0) { p.seek(36); input = p.readVarBytes(); - height = bcoin.script.getCoinbaseHeight(input); + height = Script.getCoinbaseHeight(input); } } @@ -183,7 +187,7 @@ MemBlock.prototype.toNormal = function toNormal() { */ MemBlock.prototype.toBlock = function toBlock() { - var block = bcoin.block.fromRaw(this.raw); + var block = Block.fromRaw(this.raw); block._hash = this._hash; block._cbHeight = this.coinbaseHeight; this.raw = null; diff --git a/lib/primitives/merkleblock.js b/lib/primitives/merkleblock.js index a219dc1a..0f6a47fc 100644 --- a/lib/primitives/merkleblock.js +++ b/lib/primitives/merkleblock.js @@ -7,14 +7,18 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = MerkleBlock; + var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); var DUMMY = new Buffer([0]); -var AbstractBlock = bcoin.abstractblock; +var AbstractBlock = require('./abstractblock'); var VerifyResult = utils.VerifyResult; +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); +var TX = require('./tx'); /** * Represents a merkle (filtered) block. @@ -91,7 +95,7 @@ MerkleBlock.fromOptions = function fromOptions(data) { */ MerkleBlock.prototype.getSize = function getSize() { - var writer = new bcoin.writer(); + var writer = new BufferWriter(); this.toRaw(writer); return writer.written; }; @@ -131,7 +135,7 @@ MerkleBlock.prototype.hasTX = function hasTX(hash) { MerkleBlock.prototype.indexOf = function indexOf(hash) { var index; - if (hash instanceof bcoin.tx) + if (hash instanceof TX) hash = hash.hash('hex'); this.verifyPartial(); @@ -341,7 +345,7 @@ MerkleBlock.prototype.inspect = function inspect() { */ MerkleBlock.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i; p.writeU32(this.version); @@ -372,7 +376,7 @@ MerkleBlock.prototype.toRaw = function toRaw(writer) { */ MerkleBlock.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, hashCount; this.version = p.readU32(); diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index be32a799..caf87678 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -7,15 +7,24 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = MTX; + var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; -var Script = bcoin.script; -var opcodes = constants.opcodes; -var FundingError = bcoin.errors.FundingError; -var TX = bcoin.tx; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var Network = require('../protocol/network'); +var Script = require('../script/script'); +var opcodes = Script.opcodes; +var FundingError = require('../utils/errors').FundingError; +var TX = require('./tx'); +var Input = require('./input'); +var Output = require('./output'); +var Coin = require('./coin'); +var KeyRing = require('./keyring'); +var Address = require('./address'); +var ec = require('../crypto/ec'); +var workers = require('../workers/workers'); /** * A mutable transaction object. @@ -150,12 +159,12 @@ MTX.prototype.clone = function clone() { */ MTX.prototype.addInput = function addInput(options, index) { - var input = new bcoin.input(); + var input = new Input(); input.mutable = true; if (options instanceof TX) input.fromTX(options, index); - else if (options instanceof bcoin.coin) + else if (options instanceof Coin) input.fromCoin(options); else input.fromOptions(options); @@ -179,18 +188,16 @@ MTX.prototype.addInput = function addInput(options, index) { MTX.prototype.addOutput = function addOutput(options, value) { var output; - if ((options instanceof bcoin.wallet) - || (options instanceof bcoin.keyring)) { + if (options instanceof KeyRing) options = options.getAddress(); - } if (typeof options === 'string') - options = bcoin.address.fromBase58(options); + options = Address.fromBase58(options); - if (options instanceof bcoin.address) + if (options instanceof Address) options = Script.fromAddress(options); - output = new bcoin.output(); + output = new Output(); output.mutable = true; if (options instanceof Script) { @@ -385,6 +392,19 @@ MTX.prototype.scriptVector = function scriptVector(prev, vector, ring) { return false; }; +/** + * Sign a transaction input on the worker pool + * (if workers are enabled). + * @param {Number} index + * @param {Buffer} key + * @param {SighashType?} type + * @returns {Promise} + */ + +MTX.prototype.signInputAsync = function signInputAsync(index, key, type) { + return workers.pool.signInput(this, index, key, type); +}; + /** * Sign an input. * @param {Number} index - Index of input being signed. @@ -462,7 +482,7 @@ MTX.prototype.signInput = function signInput(index, key, type) { */ MTX.prototype.signVector = function signVector(prev, vector, sig, key) { - var pub = bcoin.ec.publicKeyCreate(key, true); + var pub = ec.publicKeyCreate(key, true); var i, m, n, keys, keyIndex, total; // P2PK @@ -820,6 +840,12 @@ MTX.prototype.template = function template(ring) { var total = 0; var i; + if (Array.isArray(ring)) { + for (i = 0; i < ring.length; i++) + total += this.template(ring[i]); + return total; + } + for (i = 0; i < this.inputs.length; i++) { if (!ring.ownInput(this, i)) continue; @@ -879,29 +905,12 @@ MTX.prototype.sign = function sign(ring, type) { * (if workers are enabled). * @param {KeyRing} ring * @param {SighashType?} type - * @param {Function} callback + * @returns {Promise} * @returns {Boolean} Whether the inputs are valid. */ -MTX.prototype.signAsync = function signAsync(ring, type, callback) { - var result; - - if (typeof type === 'function') { - callback = type; - type = null; - } - - if (!bcoin.useWorkers) { - callback = utils.asyncify(callback); - try { - result = this.sign(ring, type); - } catch (e) { - return callback(e); - } - return callback(null, result); - } - - bcoin.workerPool.sign(this, ring, type, callback); +MTX.prototype.signAsync = function signAsync(ring, type) { + return workers.pool.sign(this, ring, type); }; /** @@ -1002,7 +1011,7 @@ MTX.prototype.maxSize = function maxSize(options) { if (redeem) { prev = redeem; sz = prev.getSize(); - size += bcoin.script.sizePush(sz); + size += Script.sizePush(sz); size += sz; } } @@ -1124,8 +1133,8 @@ MTX.prototype._guessRedeem = function guessRedeem(options, hash) { case 20: if (options.witness) { if (options.n > 1) - return bcoin.script.fromProgram(0, constants.ZERO_HASH); - return bcoin.script.fromProgram(0, constants.ZERO_HASH160); + return Script.fromProgram(0, constants.ZERO_HASH); + return Script.fromProgram(0, constants.ZERO_HASH160); } return options.script; case 32: @@ -1175,7 +1184,7 @@ MTX.prototype.subtractFee = function subtractFee(fee, index) { if (Array.isArray(index)) { addrs = []; for (i = 0; i < index.length; i++) { - hash = bcoin.address.getHash(index[i]); + hash = Address.getHash(index[i]); if (hash) addrs.push(hash); } @@ -1308,7 +1317,7 @@ MTX.prototype.sortMembers = function sortMembers() { MTX.prototype.avoidFeeSniping = function avoidFeeSniping(height) { if (height == null) - height = bcoin.network.get().height; + height = Network.primary.height; if (height === -1) height = 0; @@ -1533,9 +1542,9 @@ CoinSelector.prototype.fromOptions = function fromOptions(options) { if (options.changeAddress) { addr = options.changeAddress; if (typeof addr === 'string') { - this.changeAddress = bcoin.address.fromBase58(addr); + this.changeAddress = Address.fromBase58(addr); } else { - assert(addr instanceof bcoin.address); + assert(addr instanceof Address); this.changeAddress = addr; } } @@ -1558,7 +1567,7 @@ CoinSelector.prototype.fromOptions = function fromOptions(options) { } if (options.script) { - assert(options.script instanceof bcoin.script); + assert(options.script instanceof Script); this.script = options.script; } @@ -1660,9 +1669,9 @@ CoinSelector.prototype.getFee = function getFee(size) { var fee; if (this.round) - fee = bcoin.tx.getRoundFee(size, this.rate); + fee = TX.getRoundFee(size, this.rate); else - fee = bcoin.tx.getMinFee(size, this.rate); + fee = TX.getMinFee(size, this.rate); if (fee > constants.tx.MAX_FEE) fee = constants.tx.MAX_FEE; diff --git a/lib/primitives/netaddress.js b/lib/primitives/netaddress.js index 454d2e89..11a01d02 100644 --- a/lib/primitives/netaddress.js +++ b/lib/primitives/netaddress.js @@ -6,11 +6,16 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = NetworkAddress; + var constants = require('../protocol/constants'); +var Network = require('../protocol/network'); +var time = require('../net/timedata'); var utils = require('../utils/utils'); var IP = require('../utils/ip'); -var assert = utils.assert; +var assert = require('assert'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); /** * Represents a network address. @@ -183,14 +188,14 @@ NetworkAddress.prototype.inspect = function inspect() { NetworkAddress.prototype.fromHostname = function fromHostname(hostname, network) { var address = IP.parseHost(hostname); - network = bcoin.network.get(network); + network = Network.get(network); this.host = address.host; this.port = address.port || network.port; this.services = constants.services.NETWORK | constants.services.BLOOM | constants.services.WITNESS; - this.ts = bcoin.now(); + this.ts = time.now(); this.hostname = IP.hostname(this.host, this.port); @@ -224,7 +229,7 @@ NetworkAddress.prototype.fromSocket = function fromSocket(socket) { this.services = constants.services.NETWORK | constants.services.BLOOM | constants.services.WITNESS; - this.ts = bcoin.now(); + this.ts = time.now(); this.hostname = IP.hostname(this.host, this.port); @@ -250,8 +255,8 @@ NetworkAddress.fromSocket = function fromSocket(hostname) { */ NetworkAddress.prototype.fromRaw = function fromRaw(data, full) { - var p = bcoin.reader(data); - var now = bcoin.now(); + var p = BufferReader(data); + var now = time.now(); // only version >= 31402 this.ts = full ? p.readU32() : 0; @@ -285,7 +290,7 @@ NetworkAddress.fromRaw = function fromRaw(data, full) { */ NetworkAddress.prototype.toRaw = function toRaw(full, writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); if (full) p.writeU32(this.ts); diff --git a/lib/primitives/outpoint.js b/lib/primitives/outpoint.js index 34f1f50a..ce17d441 100644 --- a/lib/primitives/outpoint.js +++ b/lib/primitives/outpoint.js @@ -6,10 +6,13 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Outpoint; + var utils = require('../utils/utils'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); /** * Represents a COutPoint. @@ -69,7 +72,7 @@ Outpoint.prototype.isNull = function isNull() { */ Outpoint.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.writeHash(this.hash); p.writeU32(this.index); @@ -87,7 +90,7 @@ Outpoint.prototype.toRaw = function toRaw(writer) { */ Outpoint.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.hash = p.readHash('hex'); this.index = p.readU32(); return this; diff --git a/lib/primitives/output.js b/lib/primitives/output.js index 3d15e8ed..cbcf53de 100644 --- a/lib/primitives/output.js +++ b/lib/primitives/output.js @@ -7,10 +7,15 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Output; + var utils = require('../utils/utils'); -var constants = bcoin.constants; -var assert = utils.assert; +var constants = require('../protocol/constants'); +var Script = require('../script/script'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); +var assert = require('assert'); +var TX = require('./tx'); /** * Represents a transaction output. @@ -26,7 +31,7 @@ function Output(options) { return new Output(options); this.value = 0; - this.script = new bcoin.script(); + this.script = new Script(); this.mutable = false; this._address = null; @@ -165,7 +170,7 @@ Output.prototype.getDustThreshold = function getDustThreshold(rate) { size += 32 + 4 + 1 + 107 + 4; } - return 3 * bcoin.tx.getMinFee(size, rate); + return 3 * TX.getMinFee(size, rate); }; /** @@ -174,7 +179,7 @@ Output.prototype.getDustThreshold = function getDustThreshold(rate) { */ Output.prototype.getSize = function getSize() { - return this.toRaw(bcoin.writer()).written; + return this.toRaw(BufferWriter()).written; }; /** @@ -217,7 +222,7 @@ Output.fromJSON = function fromJSON(json) { */ Output.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); p.write64(this.value); p.writeVarBytes(this.script.toRaw()); @@ -235,7 +240,7 @@ Output.prototype.toRaw = function toRaw(writer) { */ Output.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); this.value = p.read64N(); this.script.fromRaw(p.readVarBytes()); diff --git a/lib/primitives/tx.js b/lib/primitives/tx.js index 7f59bbcd..b0f791be 100644 --- a/lib/primitives/tx.js +++ b/lib/primitives/tx.js @@ -7,15 +7,25 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = TX; + var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; -var Script = bcoin.script; -var Stack = bcoin.stack; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var Network = require('../protocol/network'); +var Script = require('../script/script'); +var Stack = require('../script/stack'); var BufferWriter = require('../utils/writer'); var VerifyResult = utils.VerifyResult; +var Input = require('./input'); +var Output = require('./output'); +var Outpoint = require('./outpoint'); +var Coin = require('./coin'); +var InvItem = require('./invitem'); +var workers = require('../workers/workers'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); /* * Constants @@ -117,13 +127,13 @@ TX.prototype.fromOptions = function fromOptions(options) { if (options.inputs) { assert(Array.isArray(options.inputs)); for (i = 0; i < options.inputs.length; i++) - this.inputs.push(new bcoin.input(options.inputs[i])); + this.inputs.push(new Input(options.inputs[i])); } if (options.outputs) { assert(Array.isArray(options.outputs)); for (i = 0; i < options.outputs.length; i++) - this.outputs.push(new bcoin.output(options.outputs[i])); + this.outputs.push(new Output(options.outputs[i])); } if (options.locktime != null) { @@ -474,7 +484,7 @@ TX.prototype.signatureHashV0 = function signatureHashV0(index, prev, type) { // Remove all code separators. prev = prev.removeSeparators(); - p.write32(this.version); + p.writeU32(this.version); if (type & constants.hashType.ANYONECANPAY) { p.writeVarint(1); @@ -628,7 +638,7 @@ TX.prototype.signatureHashV1 = function signatureHashV1(index, prev, type) { hashOutputs = utils.copy(constants.ZERO_HASH); } - p.write32(this.version); + p.writeU32(this.version); p.writeBytes(hashPrevouts); p.writeBytes(hashSequence); p.writeHash(this.inputs[index].prevout.hash); @@ -709,39 +719,39 @@ TX.prototype.verifyInput = function verifyInput(index, flags) { * Verify the transaction inputs on the worker pool * (if workers are enabled). * @param {VerifyFlags?} [flags=STANDARD_VERIFY_FLAGS] - * @param {Function} callback + * @returns {Promise} * @returns {Boolean} Whether the inputs are valid. */ -TX.prototype.verifyAsync = function verifyAsync(flags, callback) { - var result; +TX.prototype.verifyAsync = function verifyAsync(flags) { + if (this.inputs.length === 0) + return Promise.resolve(false); - if (typeof flags === 'function') { - callback = flags; - flags = null; - } + if (this.isCoinbase()) + return Promise.resolve(true); - if (!bcoin.useWorkers) { - callback = utils.asyncify(callback); - try { - result = this.verify(flags); - } catch (e) { - return callback(e); - } - return callback(null, result); - } + return workers.pool.verify(this, flags); +}; - if (this.inputs.length === 0) { - callback = utils.asyncify(callback); - return callback(null, false); - } +/** + * Verify a transaction input asynchronously. + * @param {Number} index - Index of output being + * verified. + * @param {VerifyFlags} [flags=STANDARD_VERIFY_FLAGS] + * @returns {Boolean} Whether the input is valid. + */ - if (this.isCoinbase()) { - callback = utils.asyncify(callback); - return callback(null, true); - } +TX.prototype.verifyInputAsync = function verifyInputAsync(index, flags) { + var input; - bcoin.workerPool.verify(this, flags, callback); + if (typeof index === 'object') + index = this.inputs.indexOf(index); + + input = this.inputs[index]; + + assert(input, 'Input does not exist.'); + + return workers.pool.verifyInput(this, index, flags); }; /** @@ -968,7 +978,7 @@ TX.prototype.fillCoins = function fillCoins(coins) { var result = true; var i, input, hash, index, map, coin; - if ((coins instanceof bcoin.coin) + if ((coins instanceof Coin) || (coins instanceof TX)) { coins = [coins]; } @@ -979,7 +989,7 @@ TX.prototype.fillCoins = function fillCoins(coins) { coin = coins[i]; if (coin instanceof TX) { map[coin.hash('hex')] = coin; - } else if (coin instanceof bcoin.coin) { + } else if (coin instanceof Coin) { assert(typeof coin.hash === 'string'); assert(typeof coin.index === 'number'); map[coin.hash + coin.index] = coin; @@ -1001,7 +1011,7 @@ TX.prototype.fillCoins = function fillCoins(coins) { coin = coins[hash]; if (coin) { - input.coin = bcoin.coin.fromTX(coin, index); + input.coin = Coin.fromTX(coin, index); continue; } @@ -1486,7 +1496,7 @@ TX.prototype.getWitnessStandard = function getWitnessStandard() { ret = BAD_NONSTD_P2WSH; } - redeem = new bcoin.script(redeem); + redeem = new Script(redeem); if (redeem.isPubkey()) { if (input.witness.length - 1 !== 1) @@ -1662,7 +1672,7 @@ TX.prototype.getPriority = function getPriority(height, size) { if (height == null) { height = this.height; if (height === -1) - height = bcoin.network.get().height; + height = Network.primary.height; } if (size == null) @@ -1791,7 +1801,7 @@ TX.prototype.getRate = function getRate(size) { TX.prototype.getConfirmations = function getConfirmations(height) { if (height == null) - height = bcoin.network.get().height; + height = Network.primary.height; if (this.height === -1) return 0; @@ -1851,11 +1861,11 @@ TX.prototype.isWatched = function isWatched(filter) { // Test the output script if (output.script.test(filter)) { if (filter.update === constants.filterFlags.ALL) { - outpoint = bcoin.outpoint.fromTX(this, i); + outpoint = Outpoint.fromTX(this, i); filter.add(outpoint.toRaw()); } else if (filter.update === constants.filterFlags.PUBKEY_ONLY) { if (output.script.isPubkey() || output.script.isMultisig()) { - outpoint = bcoin.outpoint.fromTX(this, i); + outpoint = Outpoint.fromTX(this, i); filter.add(outpoint.toRaw()); } } @@ -1917,7 +1927,7 @@ TX.prototype.__defineGetter__('wtxid', function() { */ TX.prototype.toInv = function toInv() { - return new bcoin.invitem(constants.inv.TX, this.hash('hex')); + return new InvItem(constants.inv.TX, this.hash('hex')); }; /** @@ -2074,12 +2084,12 @@ TX.prototype.fromJSON = function fromJSON(json) { for (i = 0; i < json.inputs.length; i++) { input = json.inputs[i]; - this.inputs.push(bcoin.input.fromJSON(input)); + this.inputs.push(Input.fromJSON(input)); } for (i = 0; i < json.outputs.length; i++) { output = json.outputs[i]; - this.outputs.push(bcoin.output.fromJSON(output)); + this.outputs.push(Output.fromJSON(output)); } this.locktime = json.locktime; @@ -2118,25 +2128,25 @@ TX.fromRaw = function fromRaw(data, enc) { */ TX.prototype.fromRaw = function fromRaw(data) { - var p, i, inCount, outCount; + var p, i, count; if (TX.isWitness(data)) return this.fromWitness(data); - p = bcoin.reader(data); + p = BufferReader(data); p.start(); this.version = p.readU32(); // Technically signed - inCount = p.readVarint(); + count = p.readVarint(); - for (i = 0; i < inCount; i++) - this.inputs.push(bcoin.input.fromRaw(p)); + for (i = 0; i < count; i++) + this.inputs.push(Input.fromRaw(p)); - outCount = p.readVarint(); + count = p.readVarint(); - for (i = 0; i < outCount; i++) - this.outputs.push(bcoin.output.fromRaw(p)); + for (i = 0; i < count; i++) + this.outputs.push(Output.fromRaw(p)); this.locktime = p.readU32(); @@ -2159,49 +2169,61 @@ TX.prototype.fromRaw = function fromRaw(data) { */ TX.prototype.fromWitness = function fromWitness(data) { - var p = bcoin.reader(data); - var i, marker, inCount, outCount, input, hasWitness, witnessSize; + var p = BufferReader(data); + var flag = 0; + var witnessSize = 0; + var hasWitness = false; + var i, count, input; p.start(); this.version = p.readU32(); // Technically signed - marker = p.readU8(); - this.flag = p.readU8(); + assert(p.readU8() === 0, 'Non-zero marker.'); - if (marker !== 0) - throw new Error('Invalid witness tx (marker != 0)'); + flag = p.readU8(); - if (this.flag === 0) - throw new Error('Invalid witness tx (flag == 0)'); + assert(flag !== 0, 'Flag byte is zero.'); - inCount = p.readVarint(); + this.flag = flag; - for (i = 0; i < inCount; i++) - this.inputs.push(bcoin.input.fromRaw(p)); + count = p.readVarint(); - outCount = p.readVarint(); + for (i = 0; i < count; i++) + this.inputs.push(Input.fromRaw(p)); - for (i = 0; i < outCount; i++) - this.outputs.push(bcoin.output.fromRaw(p)); + count = p.readVarint(); - p.start(); + for (i = 0; i < count; i++) + this.outputs.push(Output.fromRaw(p)); - for (i = 0; i < inCount; i++) { - input = this.inputs[i]; - input.witness.fromRaw(p); - if (input.witness.items.length > 0) - hasWitness = true; + if (flag & 1) { + flag ^= 1; + + p.start(); + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + input.witness.fromRaw(p); + if (input.witness.items.length > 0) + hasWitness = true; + } + + witnessSize = p.end() + 2; } - if (!hasWitness) - throw new Error('Witness tx has an empty witness.'); + if (flag !== 0) + throw new Error('Unknown witness flag.'); - witnessSize = p.end() + 2; + // We'll never be able to reserialize + // this to get the regular txid, and + // there's no way it's valid anyway. + if (this.inputs.length === 0 && this.outputs.length !== 0) + throw new Error('Zero input witness tx.'); this.locktime = p.readU32(); - if (!this.mutable) { + if (!this.mutable && hasWitness) { this._raw = p.endData(); this._size = this._raw.length; this._witnessSize = witnessSize; @@ -2212,27 +2234,6 @@ TX.prototype.fromWitness = function fromWitness(data) { return this; }; -/** - * Test whether data is a witness transaction. - * @param {Buffer|BufferReader} data - * @returns {Boolean} - */ - -TX.isWitness = function isWitness(data) { - if (Buffer.isBuffer(data)) { - if (data.length < 12) - return false; - - return data[4] === 0 && data[5] !== 0; - } - - if (data.left() < 12) - return false; - - return data.data[data.offset + 4] === 0 - && data.data[data.offset + 5] !== 0; -}; - /** * Serialize transaction without witness. * @private @@ -2241,13 +2242,13 @@ TX.isWitness = function isWitness(data) { */ TX.prototype.frameNormal = function frameNormal(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i; - if (this.inputs.length === 0 && this.outputs.length === 1) + if (this.inputs.length === 0 && this.outputs.length !== 0) throw new Error('Cannot serialize zero-input tx.'); - p.write32(this.version); + p.writeU32(this.version); p.writeVarint(this.inputs.length); @@ -2278,11 +2279,14 @@ TX.prototype.frameNormal = function frameNormal(writer) { */ TX.prototype.frameWitness = function frameWitness(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var witnessSize = 0; var i, start; - p.write32(this.version); + if (this.inputs.length === 0 && this.outputs.length !== 0) + throw new Error('Cannot serialize zero-input tx.'); + + p.writeU32(this.version); p.writeU8(0); p.writeU8(this.flag); @@ -2316,6 +2320,27 @@ TX.prototype.frameWitness = function frameWitness(writer) { return p; }; +/** + * Test whether data is a witness transaction. + * @param {Buffer|BufferReader} data + * @returns {Boolean} + */ + +TX.isWitness = function isWitness(data) { + if (Buffer.isBuffer(data)) { + if (data.length < 6) + return false; + + return data[4] === 0 && data[5] !== 0; + } + + if (data.left() < 6) + return false; + + return data.data[data.offset + 4] === 0 + && data.data[data.offset + 5] !== 0; +}; + /** * Serialize a transaction to BCoin "extended format". * This is the serialization format BCoin uses internally @@ -2331,7 +2356,7 @@ TX.prototype.frameWitness = function frameWitness(writer) { TX.prototype.toExtended = function toExtended(saveCoins, writer) { var height = this.height; var index = this.index; - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i, input; if (height === -1) @@ -2377,7 +2402,7 @@ TX.prototype.toExtended = function toExtended(saveCoins, writer) { */ TX.prototype.fromExtended = function fromExtended(data, saveCoins) { - var p = bcoin.reader(data); + var p = BufferReader(data); var i, coinCount, coin; this.fromRaw(p); @@ -2403,7 +2428,7 @@ TX.prototype.fromExtended = function fromExtended(data, saveCoins) { coin = p.readVarBytes(); if (coin.length === 0) continue; - coin = bcoin.coin.fromRaw(coin); + coin = Coin.fromRaw(coin); coin.hash = this.inputs[i].prevout.hash; coin.index = this.inputs[i].prevout.index; this.inputs[i].coin = coin; diff --git a/lib/protocol/constants.js b/lib/protocol/constants.js index dadbb30f..c49f4697 100644 --- a/lib/protocol/constants.js +++ b/lib/protocol/constants.js @@ -775,8 +775,9 @@ exports.flags = { VERIFY_CHECKSEQUENCEVERIFY: (1 << 10), VERIFY_WITNESS: (1 << 11), VERIFY_DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM: (1 << 12), - VERIFY_MAST: (1 << 13), + VERIFY_MINIMALIF: (1 << 13), VERIFY_NULLFAIL: (1 << 14), + VERIFY_MAST: (1 << 15), // should be 1 << 13 VERIFY_SEQUENCE: (1 << 0), MEDIAN_TIME_PAST: (1 << 1) }; diff --git a/lib/protocol/index.js b/lib/protocol/index.js new file mode 100644 index 00000000..cf5a4c6a --- /dev/null +++ b/lib/protocol/index.js @@ -0,0 +1,5 @@ +'use strict'; + +exports.constants = require('./constants'); +exports.network = require('./network'); +exports.networks = require('./networks'); diff --git a/lib/protocol/network.js b/lib/protocol/network.js index 352b7ecf..46363e3c 100644 --- a/lib/protocol/network.js +++ b/lib/protocol/network.js @@ -7,8 +7,7 @@ 'use strict'; -var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); var networks = require('./networks'); /** @@ -16,10 +15,6 @@ var networks = require('./networks'); * @exports Network * @constructor * @param {Object|NetworkType} options - See {@link module:network}. - * @property {Number} height - * @property {Rate} feeRate - * @property {Rate} minRelay - * @property {PolicyEstimator} fees */ function Network(options) { @@ -51,8 +46,6 @@ function Network(options) { this.rpcPort = options.rpcPort; this.minRelay = options.minRelay; this.feeRate = options.feeRate; - this.minRate = options.minRate; - this.maxRate = options.maxRate; this.selfConnect = options.selfConnect; this.requestMempool = options.requestMempool; this.batchSize = options.batchSize; @@ -60,11 +53,18 @@ function Network(options) { /** * Default network. - * @type {String} + * @type {Network} */ Network.primary = null; +/** + * Default network type. + * @type {String} + */ + +Network.type = null; + /* * Networks (to avoid hash table mode). */ @@ -85,50 +85,6 @@ Network.prototype.updateHeight = function updateHeight(height) { this.height = height; }; -/** - * Update the estimated fee rate of the network. - * @param {Rate} rate - */ - -Network.prototype.updateRate = function updateRate(rate) { - this.feeRate = rate; -}; - -/** - * Update the minimum relay rate (reject rate) of the network. - * @param {Rate} rate - */ - -Network.prototype.updateMinRelay = function updateMinRelay(rate) { - this.minRelay = rate; -}; - -/** - * Calculate the minimum relay rate. If the network is - * inactive (height=-1), return the default minimum relay. - * @return {Rate} Rate - */ - -Network.prototype.getMinRelay = function getMinRelay() { - if (this.height === -1) - return this.minRate; - - return Math.min(this.minRelay, this.maxRate); -}; - -/** - * Calculate the normal relay rate. If the network is - * inactive (height=-1), return the default rate. - * @return {Rate} Rate - */ - -Network.prototype.getRate = function getRate() { - if (this.height === -1) - return this.maxRate; - - return Math.min(this.feeRate, this.maxRate); -}; - /** * Determine how many blocks to request * based on current height of the chain. @@ -187,6 +143,7 @@ Network.create = function create(options) { Network.set = function set(type) { assert(typeof type === 'string', 'Bad network.'); Network.primary = Network.get(type); + Network.type = type; return Network.primary; }; @@ -261,6 +218,12 @@ Network.isNetwork = function isNetwork(obj) { && typeof obj.pow === 'object'; }; +/* + * Set initial network. + */ + +Network.set(process.env.BCOIN_NETWORK || 'main'); + /* * Expose */ diff --git a/lib/protocol/networks.js b/lib/protocol/networks.js index 14a8d2cf..67660013 100644 --- a/lib/protocol/networks.js +++ b/lib/protocol/networks.js @@ -399,7 +399,7 @@ main.requireStandard = true; main.rpcPort = 8332; /** - * Default min relay rate (the rate for mempoolRejectFee). + * Default min relay rate. * @const {Rate} * @default */ @@ -414,22 +414,6 @@ main.minRelay = 10000; main.feeRate = 50000; -/** - * Default min rate. - * @const {Rate} - * @default - */ - -main.minRate = 10000; - -/** - * Default max rate. - * @const {Rate} - * @default - */ - -main.maxRate = 50000; - /** * Whether to allow self-connection. * @const {Boolean} @@ -597,10 +581,6 @@ testnet.minRelay = 10000; testnet.feeRate = 20000; -testnet.minRate = 10000; - -testnet.maxRate = 40000; - testnet.selfConnect = false; testnet.requestMempool = false; @@ -743,10 +723,6 @@ regtest.minRelay = 10000; regtest.feeRate = 20000; -regtest.minRate = 10000; - -regtest.maxRate = 40000; - regtest.selfConnect = true; regtest.requestMempool = true; @@ -863,10 +839,6 @@ segnet3.minRelay = 10000; segnet3.feeRate = 20000; -segnet3.minRate = 10000; - -segnet3.maxRate = 40000; - segnet3.selfConnect = false; segnet3.requestMempool = true; @@ -1002,10 +974,6 @@ segnet4.minRelay = 10000; segnet4.feeRate = 20000; -segnet4.minRate = 10000; - -segnet4.maxRate = 40000; - segnet4.selfConnect = false; segnet4.requestMempool = true; @@ -1149,10 +1117,6 @@ simnet.minRelay = 10000; simnet.feeRate = 20000; -simnet.minRate = 10000; - -simnet.maxRate = 40000; - simnet.selfConnect = true; simnet.requestMempool = false; diff --git a/lib/script/index.js b/lib/script/index.js new file mode 100644 index 00000000..7d380683 --- /dev/null +++ b/lib/script/index.js @@ -0,0 +1,8 @@ +'use strict'; + +exports.Opcode = require('./opcode'); +exports.Program = require('./program'); +exports.Script = require('./script'); +exports.SigCache = require('./sigcache'); +exports.Stack = require('./stack'); +exports.Witness = require('./witness'); diff --git a/lib/script/opcode.js b/lib/script/opcode.js index da147731..4db6c9a5 100644 --- a/lib/script/opcode.js +++ b/lib/script/opcode.js @@ -7,11 +7,13 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Opcode; + var bn = require('bn.js'); -var constants = bcoin.constants; +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); -var assert = utils.assert; +var Script = require('./script'); +var assert = require('assert'); var opcodes = constants.opcodes; /** @@ -39,7 +41,7 @@ function Opcode(value, data) { */ Opcode.prototype.toRaw = function toRaw(writer) { - return bcoin.script.encode([this], writer); + return Script.encode([this], writer); }; /** @@ -105,7 +107,7 @@ Opcode.fromPush = function fromPush(data) { */ Opcode.fromNumber = function fromNumber(num) { - return Opcode.fromData(bcoin.script.array(num)); + return Opcode.fromData(Script.array(num)); }; /** diff --git a/lib/script/program.js b/lib/script/program.js index 1d3ee8ee..b69ba8d1 100644 --- a/lib/script/program.js +++ b/lib/script/program.js @@ -7,10 +7,11 @@ 'use strict'; -var bcoin = require('../env'); -var constants = bcoin.constants; +module.exports = Program; + +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); var scriptTypes = constants.scriptTypes; /** diff --git a/lib/script/script.js b/lib/script/script.js index f8421663..74e16a14 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -7,23 +7,27 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Script; + var bn = require('bn.js'); -var constants = bcoin.constants; +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; +var assert = require('assert'); var BufferWriter = require('../utils/writer'); var BufferReader = require('../utils/reader'); var opcodes = constants.opcodes; var STACK_TRUE = new Buffer([1]); -var STACK_FALSE = new Buffer([]); +var STACK_FALSE = new Buffer(0); var STACK_NEGATE = new Buffer([0x81]); -var ScriptError = bcoin.errors.ScriptError; +var ScriptError = require('../utils/errors').ScriptError; var scriptTypes = constants.scriptTypes; var Program = require('./program'); var Opcode = require('./opcode'); var Stack = require('./stack'); +var SigCache = require('./sigcache'); +var ec = require('../crypto/ec'); +var Address = require('../primitives/address'); /** * Represents a input or output script. @@ -297,11 +301,13 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { var alt = []; var state = []; var negate = 0; - var op, code, data, val, v1, v2, v3; - var n, n1, n2, n3; - var res, key, sig, type, subscript, hash; - var i, j, m, ikey, ikey2, isig; - var locktime; + var op, code, data; + var val, v1, v2, v3; + var num, n1, n2, n3; + var m, n, key, sig; + var type, subscript, hash; + var ikey, isig, ikey2; + var i, j, res, locktime; if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; @@ -320,6 +326,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { if (data) { if (data.length > constants.script.MAX_PUSH) throw new ScriptError('PUSH_SIZE', op, ip); + // Note that minimaldata is not checked // on unexecuted branches of code. if (negate === 0) { @@ -327,6 +334,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { throw new ScriptError('MINIMALDATA', op, ip); stack.push(data); } + continue; } @@ -362,33 +370,56 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { case opcodes.OP_IF: case opcodes.OP_NOTIF: { val = false; + if (negate === 0) { if (stack.length < 1) throw new ScriptError('UNBALANCED_CONDITIONAL', op, ip); - val = Script.bool(stack.pop()); + + val = stack.top(-1); + + if (version === 1 && (flags & constants.flags.VERIFY_MINIMALIF)) { + if (val.length > 1) + throw new ScriptError('MINIMALIF'); + + if (val.length === 1 && val[0] !== 1) + throw new ScriptError('MINIMALIF'); + } + + val = Script.bool(val); + if (op === opcodes.OP_NOTIF) val = !val; + + stack.pop(); } + state.push(val); + if (!val) negate++; + break; } case opcodes.OP_ELSE: { if (state.length === 0) throw new ScriptError('UNBALANCED_CONDITIONAL', op, ip); + state[state.length - 1] = !state[state.length - 1]; + if (!state[state.length - 1]) negate++; else negate--; + break; } case opcodes.OP_ENDIF: { if (state.length === 0) throw new ScriptError('UNBALANCED_CONDITIONAL', op, ip); + if (!state.pop()) negate--; + break; } case opcodes.OP_VERIF: @@ -506,104 +537,209 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { case opcodes.OP_VERIFY: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - if (!Script.bool(stack.pop())) + + if (!Script.bool(stack.top(-1))) throw new ScriptError('VERIFY', op, ip); + + stack.pop(); + break; } case opcodes.OP_RETURN: { throw new ScriptError('OP_RETURN', op, ip); } case opcodes.OP_TOALTSTACK: { - stack.toalt(alt); + if (stack.length === 0) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + alt.push(stack.pop()); break; } case opcodes.OP_FROMALTSTACK: { - stack.fromalt(alt); + if (alt.length === 0) + throw new ScriptError('INVALID_ALTSTACK_OPERATION', op, ip); + + stack.push(alt.pop()); break; } case opcodes.OP_2DROP: { - stack.drop2(); + if (stack.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.pop(); + stack.pop(); break; } case opcodes.OP_2DUP: { - stack.dup2(); + if (stack.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + v1 = stack.top(-2); + v2 = stack.top(-1); + + stack.push(v1); + stack.push(v2); break; } case opcodes.OP_3DUP: { - stack.dup3(); + if (stack.length < 3) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + v1 = stack.top(-3); + v2 = stack.top(-2); + v3 = stack.top(-1); + + stack.push(v1); + stack.push(v2); + stack.push(v3); break; } case opcodes.OP_2OVER: { - stack.over2(); + if (stack.length < 4) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + v1 = stack.top(-4); + v2 = stack.top(-3); + + stack.push(v1); + stack.push(v2); break; } case opcodes.OP_2ROT: { - stack.rot2(); + if (stack.length < 6) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + v1 = stack.top(-6); + v2 = stack.top(-5); + + stack.erase(-6, -4); + stack.push(v1); + stack.push(v2); break; } case opcodes.OP_2SWAP: { - stack.swap2(); + if (stack.length < 4) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.swap(-4, -2); + stack.swap(-3, -1); break; } case opcodes.OP_IFDUP: { - stack.ifdup(); + if (stack.length === 0) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + val = stack.top(-1); + + if (Script.bool(val)) + stack.push(val); break; } case opcodes.OP_DEPTH: { - stack.depth(); + stack.push(Script.array(stack.length)); break; } case opcodes.OP_DROP: { - stack.drop(); + if (stack.length === 0) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.pop(); break; } case opcodes.OP_DUP: { - stack.dup(); + if (stack.length === 0) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.push(stack.top(-1)); break; } case opcodes.OP_NIP: { - stack.nip(); + if (stack.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.remove(-2); break; } case opcodes.OP_OVER: { - stack.over(); - break; - } - case opcodes.OP_PICK: { - stack.pick(flags); + if (stack.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.push(stack.top(-2)); break; } + case opcodes.OP_PICK: case opcodes.OP_ROLL: { - stack.roll(flags); + if (stack.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', op); + + num = Script.num(stack.top(-1), flags).toNumber(); + stack.pop(); + + if (num < 0 || num >= stack.length) + throw new ScriptError('INVALID_STACK_OPERATION', op); + + val = stack.top(-num - 1); + + if (op === opcodes.OP_ROLL) + stack.remove(-num - 1); + + stack.push(val); break; } case opcodes.OP_ROT: { - stack.rot(); + if (stack.length < 3) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.swap(-3, -2); + stack.swap(-2, -1); break; } case opcodes.OP_SWAP: { - stack.swap(); + if (stack.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.swap(-2, -1); break; } case opcodes.OP_TUCK: { - stack.tuck(); + if (stack.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.insert(-2, stack.top(-1)); break; } case opcodes.OP_SIZE: { - stack.size(); + if (stack.length < 1) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + stack.push(Script.array(stack.top(-1).length)); break; } case opcodes.OP_EQUAL: case opcodes.OP_EQUALVERIFY: { + // case opcodes.OP_NOTEQUAL: // use OP_NUMNOTEQUAL if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - res = utils.equal(stack.pop(), stack.pop()); + + v1 = stack.top(-2); + v2 = stack.top(-1); + + res = utils.equal(v1, v2); + + // if (op == opcodes.OP_NOTEQUAL) + // res = !res; + + stack.pop(); + stack.pop(); + + stack.push(res ? STACK_TRUE : STACK_FALSE); + if (op === opcodes.OP_EQUALVERIFY) { if (!res) throw new ScriptError('EQUALVERIFY', op, ip); - } else { - stack.push(res ? STACK_TRUE : STACK_FALSE); + stack.pop(); } + break; } case opcodes.OP_1ADD: @@ -616,39 +752,45 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { case opcodes.OP_0NOTEQUAL: { if (stack.length < 1) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - n = Script.num(stack.pop(), flags); + + num = Script.num(stack.top(-1), flags); + switch (op) { case opcodes.OP_1ADD: - n.iaddn(1); + num.iaddn(1); break; case opcodes.OP_1SUB: - n.isubn(1); + num.isubn(1); break; case opcodes.OP_2MUL: - n.iushln(1); + num.iushln(1); break; case opcodes.OP_2DIV: - n.iushrn(1); + num.iushrn(1); break; case opcodes.OP_NEGATE: - n.ineg(); + num.ineg(); break; case opcodes.OP_ABS: - if (n.cmpn(0) < 0) - n.ineg(); + if (num.cmpn(0) < 0) + num.ineg(); break; case opcodes.OP_NOT: - n = n.cmpn(0) === 0; + num = num.cmpn(0) === 0; + num = new bn(num ? 1 : 0); break; case opcodes.OP_0NOTEQUAL: - n = n.cmpn(0) !== 0; + num = num.cmpn(0) !== 0; + num = new bn(num ? 1 : 0); break; default: assert(false, 'Fatal script error.'); + break; } - if (typeof n === 'boolean') - n = new bn(n ? 1 : 0); - stack.push(Script.array(n)); + + stack.pop(); + stack.push(Script.array(num)); + break; } case opcodes.OP_ADD: @@ -669,101 +811,95 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { case opcodes.OP_GREATERTHANOREQUAL: case opcodes.OP_MIN: case opcodes.OP_MAX: { + if (stack.length < 2) + throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + + n1 = Script.num(stack.top(-2), flags); + n2 = Script.num(stack.top(-1), flags); + switch (op) { case opcodes.OP_ADD: - case opcodes.OP_SUB: - case opcodes.OP_MUL: - case opcodes.OP_DIV: - case opcodes.OP_MOD: - case opcodes.OP_LSHIFT: - case opcodes.OP_RSHIFT: - case opcodes.OP_BOOLAND: - case opcodes.OP_BOOLOR: - case opcodes.OP_NUMEQUAL: - case opcodes.OP_NUMEQUALVERIFY: - case opcodes.OP_NUMNOTEQUAL: - case opcodes.OP_LESSTHAN: - case opcodes.OP_GREATERTHAN: - case opcodes.OP_LESSTHANOREQUAL: - case opcodes.OP_GREATERTHANOREQUAL: - case opcodes.OP_MIN: - case opcodes.OP_MAX: - if (stack.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - n2 = Script.num(stack.pop(), flags); - n1 = Script.num(stack.pop(), flags); - n = new bn(0); - switch (op) { - case opcodes.OP_ADD: - n = n1.add(n2); - break; - case opcodes.OP_SUB: - n = n1.sub(n2); - break; - case opcodes.OP_MUL: - n = n1.mul(n2); - break; - case opcodes.OP_DIV: - n = n1.div(n2); - break; - case opcodes.OP_MOD: - n = n1.mod(n2); - break; - case opcodes.OP_LSHIFT: - if (n2.cmpn(0) < 0 || n2.cmpn(2048) > 0) - throw new ScriptError('UNKNOWN_ERROR', 'Bad shift.'); - n = n1.ushln(n2.toNumber()); - break; - case opcodes.OP_RSHIFT: - if (n2.cmpn(0) < 0 || n2.cmpn(2048) > 0) - throw new ScriptError('UNKNOWN_ERROR', 'Bad shift.'); - n = n1.ushrn(n2.toNumber()); - break; - case opcodes.OP_BOOLAND: - n = n1.cmpn(0) !== 0 && n2.cmpn(0) !== 0; - break; - case opcodes.OP_BOOLOR: - n = n1.cmpn(0) !== 0 || n2.cmpn(0) !== 0; - break; - case opcodes.OP_NUMEQUAL: - n = n1.cmp(n2) === 0; - break; - case opcodes.OP_NUMEQUALVERIFY: - n = n1.cmp(n2) === 0; - break; - case opcodes.OP_NUMNOTEQUAL: - n = n1.cmp(n2) !== 0; - break; - case opcodes.OP_LESSTHAN: - n = n1.cmp(n2) < 0; - break; - case opcodes.OP_GREATERTHAN: - n = n1.cmp(n2) > 0; - break; - case opcodes.OP_LESSTHANOREQUAL: - n = n1.cmp(n2) <= 0; - break; - case opcodes.OP_GREATERTHANOREQUAL: - n = n1.cmp(n2) >= 0; - break; - case opcodes.OP_MIN: - n = n1.cmp(n2) < 0 ? n1 : n2; - break; - case opcodes.OP_MAX: - n = n1.cmp(n2) > 0 ? n1 : n2; - break; - default: - assert(false, 'Fatal script error.'); - } - if (typeof n === 'boolean') - n = new bn(n ? 1 : 0); - if (op === opcodes.OP_NUMEQUALVERIFY) { - if (!Script.bool(n)) - throw new ScriptError('NUMEQUALVERIFY', op, ip); - } else { - stack.push(Script.array(n)); - } + num = n1.add(n2); break; + case opcodes.OP_SUB: + num = n1.sub(n2); + break; + case opcodes.OP_MUL: + num = n1.mul(n2); + break; + case opcodes.OP_DIV: + num = n1.div(n2); + break; + case opcodes.OP_MOD: + num = n1.mod(n2); + break; + case opcodes.OP_LSHIFT: + if (n2.cmpn(0) < 0 || n2.cmpn(2048) > 0) + throw new ScriptError('UNKNOWN_ERROR', 'Bad shift.'); + + num = n1.ushln(n2.toNumber()); + break; + case opcodes.OP_RSHIFT: + if (n2.cmpn(0) < 0 || n2.cmpn(2048) > 0) + throw new ScriptError('UNKNOWN_ERROR', 'Bad shift.'); + + num = n1.ushrn(n2.toNumber()); + break; + case opcodes.OP_BOOLAND: + num = n1.cmpn(0) !== 0 && n2.cmpn(0) !== 0; + num = new bn(num ? 1 : 0); + break; + case opcodes.OP_BOOLOR: + num = n1.cmpn(0) !== 0 || n2.cmpn(0) !== 0; + num = new bn(num ? 1 : 0); + break; + case opcodes.OP_NUMEQUAL: + num = n1.cmp(n2) === 0; + num = new bn(num ? 1 : 0); + break; + case opcodes.OP_NUMEQUALVERIFY: + num = n1.cmp(n2) === 0; + num = new bn(num ? 1 : 0); + break; + case opcodes.OP_NUMNOTEQUAL: + num = n1.cmp(n2) !== 0; + num = new bn(num ? 1 : 0); + break; + case opcodes.OP_LESSTHAN: + num = n1.cmp(n2) < 0; + num = new bn(num ? 1 : 0); + break; + case opcodes.OP_GREATERTHAN: + num = n1.cmp(n2) > 0; + num = new bn(num ? 1 : 0); + break; + case opcodes.OP_LESSTHANOREQUAL: + num = n1.cmp(n2) <= 0; + num = new bn(num ? 1 : 0); + break; + case opcodes.OP_GREATERTHANOREQUAL: + num = n1.cmp(n2) >= 0; + num = new bn(num ? 1 : 0); + break; + case opcodes.OP_MIN: + num = n1.cmp(n2) < 0 ? n1 : n2; + break; + case opcodes.OP_MAX: + num = n1.cmp(n2) > 0 ? n1 : n2; + break; + default: + assert(false, 'Fatal script error.'); + break; + } + + stack.pop(); + stack.pop(); + stack.push(Script.array(num)); + + if (op === opcodes.OP_NUMEQUALVERIFY) { + if (!Script.bool(stack.top(-1))) + throw new ScriptError('NUMEQUALVERIFY', op, ip); + stack.pop(); } break; @@ -771,45 +907,57 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { case opcodes.OP_WITHIN: { if (stack.length < 3) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - n3 = Script.num(stack.pop(), flags); - n2 = Script.num(stack.pop(), flags); - n1 = Script.num(stack.pop(), flags); + + n1 = Script.num(stack.top(-3), flags); + n2 = Script.num(stack.top(-2), flags); + n3 = Script.num(stack.top(-1), flags); + val = n2.cmp(n1) <= 0 && n1.cmp(n3) < 0; + + stack.pop(); + stack.pop(); + stack.pop(); + stack.push(val ? STACK_TRUE : STACK_FALSE); break; } case opcodes.OP_RIPEMD160: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + stack.push(crypto.ripemd160(stack.pop())); break; } case opcodes.OP_SHA1: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + stack.push(crypto.sha1(stack.pop())); break; } case opcodes.OP_SHA256: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + stack.push(crypto.sha256(stack.pop())); break; } case opcodes.OP_HASH160: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + stack.push(crypto.hash160(stack.pop())); break; } case opcodes.OP_HASH256: { if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); + stack.push(crypto.hash256(stack.pop())); break; } case opcodes.OP_CODESEPARATOR: { - lastSep = ip; + lastSep = ip + 1; break; } case opcodes.OP_CHECKSIG: @@ -820,32 +968,38 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - key = stack.pop(); - sig = stack.pop(); + sig = stack.top(-2); + key = stack.top(-1); + res = false; subscript = this.getSubscript(lastSep); + if (version === 0) subscript.removeData(sig); Script.validateSignature(sig, flags); Script.validateKey(key, flags); - type = sig[sig.length - 1]; - - hash = tx.signatureHash(index, subscript, type, version); - - res = Script.checksig(hash, sig, key, flags); + if (sig.length > 0) { + type = sig[sig.length - 1]; + hash = tx.signatureHash(index, subscript, type, version); + res = Script.checksig(hash, sig, key, flags); + } if (!res && (flags & constants.flags.VERIFY_NULLFAIL)) { if (sig.length !== 0) throw new ScriptError('NULLFAIL', op, ip); } + stack.pop(); + stack.pop(); + + stack.push(res ? STACK_TRUE : STACK_FALSE); + if (op === opcodes.OP_CHECKSIGVERIFY) { if (!res) throw new ScriptError('CHECKSIGVERIFY', op, ip); - } else { - stack.push(res ? STACK_TRUE : STACK_FALSE); + stack.pop(); } break; @@ -905,12 +1059,14 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { Script.validateSignature(sig, flags); Script.validateKey(key, flags); - type = sig[sig.length - 1]; - hash = tx.signatureHash(index, subscript, type, version); + if (sig.length > 0) { + type = sig[sig.length - 1]; + hash = tx.signatureHash(index, subscript, type, version); - if (Script.checksig(hash, sig, key, flags)) { - isig++; - m--; + if (Script.checksig(hash, sig, key, flags)) { + isig++; + m--; + } } ikey++; @@ -940,11 +1096,12 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { stack.pop(); + stack.push(res ? STACK_TRUE : STACK_FALSE); + if (op === opcodes.OP_CHECKMULTISIGVERIFY) { if (!res) throw new ScriptError('CHECKMULTISIGVERIFY', op, ip); - } else { - stack.push(res ? STACK_TRUE : STACK_FALSE); + stack.pop(); } break; @@ -952,52 +1109,73 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { case opcodes.OP_CAT: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - v2 = stack.pop(); - v1 = stack.pop(); - stack.push(Buffer.concat([v1, v2])); - if (stack.top(-1).length > constants.script.MAX_PUSH) - throw new ScriptError('PUSH_SIZE', op, ip); + + v1 = stack.top(-2); + v2 = stack.top(-1); + + stack.set(-2, utils.concat(v1, v2)); + + stack.pop(); + break; } case opcodes.OP_SUBSTR: { if (stack.length < 3) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - v3 = Script.num(stack.pop(), flags).toNumber(); // end - v2 = Script.num(stack.pop(), flags).toNumber(); // begin - v1 = stack.pop(); // string + + v1 = stack.top(-3); + v2 = Script.num(stack.top(-2), flags).toNumber(); + v3 = Script.num(stack.top(-1), flags).toNumber(); + if (v2 < 0 || v3 < v2) throw new ScriptError('UNKNOWN_ERROR', 'String out of range.'); + if (v2 > v1.length) v2 = v1.length; + if (v3 > v1.length) v3 = v1.length; - stack.push(v1.slice(v2, v3)); + + stack.set(-3, v1.slice(v2, v3)); + stack.pop(); + stack.pop(); + break; } case opcodes.OP_LEFT: case opcodes.OP_RIGHT: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - v2 = Script.num(stack.pop(), flags).toNumber(); // size - v1 = stack.pop(); // string + + v1 = stack.top(-2); + v2 = Script.num(stack.top(-1), flags).toNumber(); + if (v2 < 0) throw new ScriptError('UNKNOWN_ERROR', 'String size out of range.'); + if (v2 > v1.length) v2 = v1.length; + if (op === opcodes.OP_LEFT) v1 = v1.slice(0, v2); else v1 = v1.slice(v1.length - v2); - stack.push(v1); + + stack.set(-2, v1); + stack.pop(); + break; } case opcodes.OP_INVERT: { if (stack.length < 1) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - val = utils.copy(stack.pop()); + + val = utils.copy(stack.top(-1)); + stack.set(-1, val); + for (i = 0; i < val.length; i++) val[i] = ~val[i] & 0xff; - stack.push(val); + break; } case opcodes.OP_AND: @@ -1005,18 +1183,26 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { case opcodes.OP_XOR: { if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - v2 = stack.pop(); - v1 = utils.copy(stack.pop()); + + v1 = utils.copy(stack.top(-2)); + v2 = stack.top(-1); + if (v1.length < v2.length) { - v3 = new Buffer(v2.length - v1.length); + v3 = new Buffer(v2.length); v3.fill(0); - v1 = Buffer.concat([v1, v3]); + v1.copy(v3, 0); + v1 = v3; } + if (v2.length < v1.length) { - v3 = new Buffer(v1.length - v2.length); + v3 = new Buffer(v1.length); v3.fill(0); - v2 = Buffer.concat([v2, v3]); + v2.copy(v3, 0); + v2 = v3; } + + stack.set(-2, v1); + if (op === opcodes.OP_AND) { for (i = 0; i < v1.length; i++) v1[i] &= v2[i]; @@ -1027,7 +1213,9 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { for (i = 0; i < v1.length; i++) v1[i] ^= v2[i]; } - stack.push(v1); + + stack.pop(); + break; } default: { @@ -1036,7 +1224,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, version) { } } - if (stack.getSize(alt) > constants.script.MAX_STACK) + if (stack.length + alt.length > constants.script.MAX_STACK) throw new ScriptError('STACK_SIZE', op, ip); if (state.length !== 0) @@ -1120,9 +1308,6 @@ Script.checkSequence = function checkSequence(sequence, tx, i) { Script.bool = function bool(value) { var i; - if (bn.isBN(value)) - return value.cmpn(0) !== 0; - assert(Buffer.isBuffer(value)); for (i = 0; i < value.length; i++) { @@ -1199,20 +1384,17 @@ Script.num = function num(value, flags, size) { * numbers to a little-endian buffer while taking into * account negative zero, minimaldata, etc. * @example - * assert.deepEqual(Script.array(0), new Buffer([])); - * assert.deepEqual(Script.array(0xffee), new Buffer([0xee, 0xff, 0x00])); - * assert.deepEqual(Script.array(new bn(0xffee)), new Buffer([0xee, 0xff, 0x00])); - * assert.deepEqual(Script.array(new bn(0x1e).ineg()), new Buffer([0x9e])); - * @param {Buffer|Number|BN} value + * assert.deepEqual(Script.array(0), new Buffer(0)); + * assert.deepEqual(Script.array(0xffee), new Buffer('eeff00', 'hex')); + * assert.deepEqual(Script.array(new bn(0xffee)), new Buffer('eeff00', 'hex')); + * assert.deepEqual(Script.array(new bn(0x1e).ineg()), new Buffer('9e', 'hex')); + * @param {Number|BN} value * @returns {Buffer} */ Script.array = function(value) { var neg, result; - if (Buffer.isBuffer(value)) - return value; - if (utils.isNumber(value)) value = new bn(value); @@ -1274,6 +1456,8 @@ Script.prototype.removeData = function removeData(data) { op = this.code[i]; if (op.value === -1) { + // Can't reserialize + // a parse error. if (index.length > 0) index.push(i); break; @@ -1375,11 +1559,7 @@ Script.isMinimal = function isMinimal(data, opcode, flags) { Script.isCode = function isCode(raw) { var i, op, code; - if (!raw) - return false; - - if (!Buffer.isBuffer(raw)) - return false; + assert(Buffer.isBuffer(raw)); code = Script.decode(raw); @@ -1579,10 +1759,9 @@ Script.fromProgram = function fromProgram(version, data) { Script.prototype.fromAddress = function fromAddress(address) { if (typeof address === 'string') - address = bcoin.address.fromBase58(address); + address = Address.fromBase58(address); - if (!address) - throw new Error('Unknown address type.'); + assert(address instanceof Address, 'Not an address.'); if (address.type === scriptTypes.PUBKEYHASH) return this.fromPubkeyhash(address.hash); @@ -1593,7 +1772,7 @@ Script.prototype.fromAddress = function fromAddress(address) { if (address.version !== -1) return this.fromProgram(address.version, address.hash); - assert(false, 'Bad address type.'); + throw new Error('Unknown address type.'); }; /** @@ -1710,26 +1889,25 @@ Script.prototype.isStandard = function isStandard() { var type = this.getType(); var m, n; - if (type === scriptTypes.MULTISIG) { - m = Script.getSmall(this.raw[0]); - n = Script.getSmall(this.raw[this.raw.length - 2]); + switch (type) { + case scriptTypes.MULTISIG: + m = this.getSmall(0); + n = this.getSmall(this.code.length - 2); - if (n < 1 || n > 3) - return false; + if (n < 1 || n > 3) + return false; - if (m < 1 || m > n) - return false; + if (m < 1 || m > n) + return false; - return true; + return true; + case scriptTypes.NULLDATA: + if (this.raw.length > constants.script.MAX_OP_RETURN_BYTES) + return false; + return true; + default: + return type !== scriptTypes.NONSTANDARD; } - - if (type === scriptTypes.NULLDATA) { - if (this.raw.length > constants.script.MAX_OP_RETURN_BYTES) - return false; - return true; - } - - return type !== scriptTypes.NONSTANDARD; }; /** @@ -1748,7 +1926,7 @@ Script.prototype.getSize = function getSize() { */ Script.prototype.getInputAddress = function getInputAddress() { - return bcoin.address.fromInputScript(this); + return Address.fromInputScript(this); }; /** @@ -1759,7 +1937,7 @@ Script.prototype.getInputAddress = function getInputAddress() { */ Script.prototype.getAddress = function getAddress() { - return bcoin.address.fromScript(this); + return Address.fromScript(this); }; /** @@ -2539,8 +2717,7 @@ Script.validateKey = function validateKey(key, flags) { if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; - if (!Buffer.isBuffer(key)) - throw new ScriptError('BAD_OPCODE'); + assert(Buffer.isBuffer(key)); if (flags & constants.flags.VERIFY_STRICTENC) { if (!Script.isKeyEncoding(key)) @@ -2557,8 +2734,7 @@ Script.validateKey = function validateKey(key, flags) { */ Script.isKeyEncoding = function isKeyEncoding(key) { - if (!Buffer.isBuffer(key)) - return false; + assert(Buffer.isBuffer(key)); if (key.length < 33) return false; @@ -2592,8 +2768,7 @@ Script.validateSignature = function validateSignature(sig, flags) { if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; - if (!Buffer.isBuffer(sig)) - throw new ScriptError('BAD_OPCODE'); + assert(Buffer.isBuffer(sig)); // Allow empty sigs if (sig.length === 0) @@ -2629,8 +2804,7 @@ Script.validateSignature = function validateSignature(sig, flags) { Script.isSignatureEncoding = function isSignatureEncoding(sig) { var lenR, lenS; - if (!Buffer.isBuffer(sig)) - return false; + assert(Buffer.isBuffer(sig)); // Format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] [sighash] // * total-length: 1-byte length descriptor of everything that follows, @@ -2720,8 +2894,7 @@ Script.isSignatureEncoding = function isSignatureEncoding(sig) { Script.isHashType = function isHashType(sig) { var type; - if (!Buffer.isBuffer(sig)) - return false; + assert(Buffer.isBuffer(sig)); if (sig.length === 0) return false; @@ -2744,7 +2917,7 @@ Script.isLowDER = function isLowDER(sig) { if (!Script.isSignatureEncoding(sig)) return false; - return bcoin.ec.isLowS(sig.slice(0, -1)); + return ec.isLowS(sig.slice(0, -1)); }; /** @@ -3124,8 +3297,7 @@ Script.getSmall = function getSmall(op) { */ Script.verify = function verify(input, witness, output, tx, i, flags) { - var copy, raw, redeem, hadWitness; - var stack = new Stack(); + var stack, copy, raw, redeem, hadWitness; if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; @@ -3135,6 +3307,9 @@ Script.verify = function verify(input, witness, output, tx, i, flags) { throw new ScriptError('SIG_PUSHONLY'); } + // Setup a stack. + stack = new Stack(); + // Execute the input script input.execute(stack, flags, tx, i, 0); @@ -3146,7 +3321,7 @@ Script.verify = function verify(input, witness, output, tx, i, flags) { output.execute(stack, flags, tx, i, 0); // Verify the stack values. - if (stack.length === 0 || !Script.bool(stack.pop())) + if (stack.length === 0 || !Script.bool(stack.top(-1))) throw new ScriptError('EVAL_FALSE'); if ((flags & constants.flags.VERIFY_WITNESS) && output.isProgram()) { @@ -3160,7 +3335,7 @@ Script.verify = function verify(input, witness, output, tx, i, flags) { Script.verifyProgram(witness, output, flags, tx, i); // Force a cleanstack - stack.length = 0; + stack.length = 1; } // If the script is P2SH, execute the real output script @@ -3184,7 +3359,7 @@ Script.verify = function verify(input, witness, output, tx, i, flags) { redeem.execute(stack, flags, tx, i, 0); // Verify the the stack values. - if (stack.length === 0 || !Script.bool(stack.pop())) + if (stack.length === 0 || !Script.bool(stack.top(-1))) throw new ScriptError('EVAL_FALSE'); if ((flags & constants.flags.VERIFY_WITNESS) && redeem.isProgram()) { @@ -3198,15 +3373,14 @@ Script.verify = function verify(input, witness, output, tx, i, flags) { Script.verifyProgram(witness, redeem, flags, tx, i); // Force a cleanstack. - stack.length = 0; + stack.length = 1; } } // Ensure there is nothing left on the stack. if (flags & constants.flags.VERIFY_CLEANSTACK) { assert((flags & constants.flags.VERIFY_P2SH) !== 0); - // assert((flags & constants.flags.VERIFY_WITNESS) !== 0); - if (stack.length !== 0) + if (stack.length !== 1) throw new ScriptError('CLEANSTACK'); } @@ -3286,7 +3460,7 @@ Script.verifyProgram = function verifyProgram(witness, output, flags, tx, i) { redeem.execute(stack, flags, tx, i, 1); // Verify the stack values. - if (stack.length !== 1 || !Script.bool(stack.pop())) + if (stack.length !== 1 || !Script.bool(stack.top(-1))) throw new ScriptError('EVAL_FALSE'); return true; @@ -3319,12 +3493,12 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, i) { if (stack.length < 4) throw new ScriptError('INVALID_MAST_STACK'); - metadata = stack.pop(); + metadata = stack.top(-1); if (metadata.length < 1 || metadata.length > 5) throw new ScriptError('INVALID_MAST_STACK'); subscripts = metadata[0]; - if (subscripts === 0 || stack.length < subscripts + 2) + if (subscripts === 0 || stack.length < subscripts + 3) throw new ScriptError('INVALID_MAST_STACK'); ops = subscripts; @@ -3346,7 +3520,7 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, i) { mastRoot.writeU32(version); - pathdata = stack.pop(); + pathdata = stack.top(-2); if (pathdata.length & 0x1f) throw new ScriptError('INVALID_MAST_STACK'); @@ -3367,7 +3541,7 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, i) { for (j = 0; j < depth; j++) path.push(pathdata.slice(j * 32, j * 32 + 32)); - posdata = stack.pop(); + posdata = stack.top(-3); if (posdata.length > 4) throw new ScriptError('INVALID_MAST_STACK'); @@ -3392,7 +3566,7 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, i) { scripts.writeBytes(output.raw); for (j = 0; j < subscripts; j++) { - script = stack.pop(); + script = stack.top(-(4 + j)); if (version === 0) { if ((scripts.written + script.length) > constants.script.MAX_SIZE) throw new ScriptError('SCRIPT_SIZE'); @@ -3411,18 +3585,20 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, i) { throw new ScriptError('WITNESS_PROGRAM_MISMATCH'); if (version === 0) { + stack.length -= 3 + subscripts; + for (j = 0; j < stack.length; j++) { if (stack.get(j).length > constants.script.MAX_PUSH) throw new ScriptError('PUSH_SIZE'); } + + output = new Script(scripts.render()); + output.execute(stack, flags, tx, i, 1); + + if (stack.length !== 0) + throw new ScriptError('EVAL_FALSE'); } - output = new Script(scripts.render()); - output.execute(stack, flags, tx, i, 1); - - if (stack.length !== 0) - throw new ScriptError('EVAL_FALSE'); - return true; }; @@ -3446,9 +3622,6 @@ Script.checksig = function checksig(msg, sig, key, flags) { if (flags == null) flags = constants.flags.STANDARD_VERIFY_FLAGS; - if (!Buffer.isBuffer(sig)) - return false; - // Attempt to normalize the signature // length before passing to elliptic. // Note: We only do this for historical data! @@ -3462,10 +3635,7 @@ Script.checksig = function checksig(msg, sig, key, flags) { if (!(flags & constants.flags.VERIFY_LOW_S)) high = true; - if (bcoin.sigcache) - return bcoin.sigcache.verify(msg, sig.slice(0, -1), key, historical, high); - - return bcoin.ec.verify(msg, sig.slice(0, -1), key, historical, high); + return SigCache.verify(msg, sig.slice(0, -1), key, historical, high); }; /** @@ -3477,7 +3647,7 @@ Script.checksig = function checksig(msg, sig, key, flags) { */ Script.sign = function sign(msg, key, type) { - var sig = bcoin.ec.sign(msg, key); + var sig = ec.sign(msg, key); var p = new BufferWriter(); // Add the sighash type as a single byte @@ -3495,7 +3665,7 @@ Script.sign = function sign(msg, key, type) { */ Script.prototype.fromRaw = function fromRaw(data) { - if (data instanceof bcoin.reader) + if (data instanceof BufferReader) data = data.readVarBytes(); this.raw = data; diff --git a/lib/script/sigcache.js b/lib/script/sigcache.js index 0df6e0e0..38de3018 100644 --- a/lib/script/sigcache.js +++ b/lib/script/sigcache.js @@ -6,9 +6,9 @@ 'use strict'; -var bcoin = require('../env'); -var utils = bcoin.utils; -var assert = utils.assert; +var utils = require('../utils/utils'); +var ec = require('../crypto/ec'); +var assert = require('assert'); /** * Signature cache. @@ -106,14 +106,14 @@ SigCache.prototype.verify = function verify(msg, sig, key, historical, high) { var hash, result; if (historical || this.size === 0) - return bcoin.ec.verify(msg, sig, key, historical, high); + return ec.verify(msg, sig, key, historical, high); hash = msg.toString('hex'); if (this.has(hash, sig, key)) return true; - result = bcoin.ec.verify(msg, sig, key, historical, high); + result = ec.verify(msg, sig, key, historical, high); if (!result) return false; @@ -152,4 +152,4 @@ SigCacheEntry.prototype.equal = function equal(sig, key) { * Expose */ -module.exports = SigCache; +module.exports = new SigCache(+process.env.BCOIN_SIGCACHE_SIZE || 0); diff --git a/lib/script/stack.js b/lib/script/stack.js index 9b07ab19..a7b56b7d 100644 --- a/lib/script/stack.js +++ b/lib/script/stack.js @@ -7,10 +7,10 @@ 'use strict'; -var bcoin = require('../env'); -var constants = bcoin.constants; -var opcodes = constants.opcodes; -var ScriptError = bcoin.errors.ScriptError; +module.exports = Stack; + +var Script = require('./script'); +var Witness = require('./witness'); /** * Represents the stack of a Script during execution. @@ -51,7 +51,7 @@ Stack.prototype.inspect = function inspect() { */ Stack.prototype.toString = function toString() { - return bcoin.witness.format(this.items); + return Witness.format(this.items); }; /** @@ -61,7 +61,7 @@ Stack.prototype.toString = function toString() { */ Stack.prototype.toASM = function toASM(decode) { - return bcoin.script.formatASM(this.items, decode); + return Script.formatASM(this.items, decode); }; /** @@ -73,7 +73,7 @@ Stack.prototype.getRedeem = function getRedeem() { var redeem = this.items[this.items.length - 1]; if (!redeem) return; - return new bcoin.script(redeem); + return new Script(redeem); }; /** @@ -85,16 +85,6 @@ Stack.prototype.clone = function clone() { return new Stack(this.items.slice()); }; -/** - * Get total size of the stack, including the alt stack. - * @param {Array} alt - Alt stack. - * @returns {Number} - */ - -Stack.prototype.getSize = function getSize(alt) { - return this.items.length + alt.length; -}; - /** * Push item onto stack. * @see Array#push @@ -122,11 +112,12 @@ Stack.prototype.unshift = function unshift(item) { * @param {Number} start * @param {Number} end * @see Array#slice - * @returns {Buffer[]} + * @returns {Stack} */ Stack.prototype.slice = function slice(start, end) { - return this.items.slice(start, end); + this.items = this.items.slice(start, end); + return this; }; /** @@ -139,11 +130,62 @@ Stack.prototype.slice = function slice(start, end) { */ Stack.prototype.splice = function splice(i, remove, insert) { + if (i < 0) + i = this.items.length + i; + if (insert === undefined) return this.items.splice(i, remove); + return this.items.splice(i, remove, insert); }; +/** + * Erase stack items. + * @param {Number} start + * @param {Number} end + * @returns {Buffer[]} + */ + +Stack.prototype.erase = function erase(start, end) { + if (start < 0) + start = this.items.length + start; + + if (end < 0) + end = this.items.length + end; + + this.items.splice(start, end - start); +}; + +/** + * Insert an item. + * @param {Number} index + * @param {Buffer} item + * @returns {Buffer} + */ + +Stack.prototype.insert = function insert(i, item) { + if (i < 0) + i = this.items.length + i; + + this.items.splice(i, 0, item); +}; + +/** + * Remove an item. + * @param {Number} index + * @returns {Buffer} + */ + +Stack.prototype.remove = function remove(i) { + if (i < 0) + i = this.items.length + i; + + if (i >= this.items.length) + return; + + return this.items.splice(i, 1)[0]; +}; + /** * Pop a stack item. * @see Array#pop @@ -203,21 +245,26 @@ Stack.prototype.clear = function clear() { */ Stack.prototype.set = function set(i, value) { + if (i < 0) + i = this.items.length + i; + return this.items[i] = value; }; /** * Swap stack values. - * @private * @param {Number} i1 - Index 1. * @param {Number} i2 - Index 2. */ -Stack.prototype._swap = function _swap(i1, i2) { +Stack.prototype.swap = function swap(i1, i2) { var v1, v2; - i1 = this.items.length + i1; - i2 = this.items.length + i2; + if (i1 < 0) + i1 = this.items.length + i1; + + if (i2 < 0) + i2 = this.items.length + i2; v1 = this.items[i1]; v2 = this.items[i2]; @@ -226,300 +273,6 @@ Stack.prototype._swap = function _swap(i1, i2) { this.items[i2] = v1; }; -/** - * Perform the OP_TOALTSTACK operation. - * @param {Array} alt - Alt stack. - * @throws {ScriptError} - */ - -Stack.prototype.toalt = function toalt(alt) { - if (this.length === 0) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_TOALTSTACK); - - alt.push(this.pop()); -}; - -/** - * Perform the OP_FROMALTSTACK operation. - * @param {Array} alt - Alt stack. - * @throws {ScriptError} - */ - -Stack.prototype.fromalt = function fromalt(alt) { - if (alt.length === 0) - throw new ScriptError('INVALID_ALTSTACK_OPERATION', opcodes.OP_FROMALTSTACK); - - this.push(alt.pop()); -}; - -/** - * Perform the OP_IFDUP operation. - * @throws {ScriptError} - */ - -Stack.prototype.ifdup = function ifdup() { - if (this.length === 0) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_IFDUP); - - if (bcoin.script.bool(this.top(-1))) - this.push(this.top(-1)); -}; - -/** - * Perform the OP_DEPTH operation. - * @throws {ScriptError} - */ - -Stack.prototype.depth = function depth() { - this.push(bcoin.script.array(this.length)); -}; - -/** - * Perform the OP_DROP operation. - * @throws {ScriptError} - */ - -Stack.prototype.drop = function drop() { - if (this.length === 0) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_DROP); - - this.pop(); -}; - -/** - * Perform the OP_DUP operation. - * @throws {ScriptError} - */ - -Stack.prototype.dup = function dup() { - if (this.length === 0) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_DUP); - - this.push(this.top(-1)); -}; - -/** - * Perform the OP_NIP operation. - * @throws {ScriptError} - */ - -Stack.prototype.nip = function nip() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_NIP); - - this.splice(this.length - 2, 1); -}; - -/** - * Perform the OP_OVER operation. - * @throws {ScriptError} - */ - -Stack.prototype.over = function over() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_OVER); - - this.push(this.top(-2)); -}; - -/** - * Perform the OP_PICK operation. - * @param {VerifyFlags} flags - * @throws {ScriptError} - */ - -Stack.prototype.pick = function pick(flags) { - return this._pickroll(opcodes.OP_PICK, flags); -}; - -/** - * Perform the OP_ROLL operation. - * @param {VerifyFlags} flags - * @throws {ScriptError} - */ - -Stack.prototype.roll = function roll(flags) { - return this._pickroll(opcodes.OP_ROLL, flags); -}; - -/** - * Perform a pick or roll. - * @private - * @param {Number} op - * @param {VerifyFlags} flags - * @throws {ScriptError} - */ - -Stack.prototype._pickroll = function pickroll(op, flags) { - var val, n; - - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', op); - - val = this.pop(); - n = bcoin.script.num(val, flags).toNumber(); - - if (n < 0 || n >= this.length) - throw new ScriptError('INVALID_STACK_OPERATION', op); - - val = this.top(-n - 1); - - if (op === opcodes.OP_ROLL) - this.splice(this.length - n - 1, 1); - - this.push(val); -}; - -/** - * Perform the OP_ROT operation. - * @throws {ScriptError} - */ - -Stack.prototype.rot = function rot() { - if (this.length < 3) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_ROT); - - this._swap(-3, -2); - this._swap(-2, -1); -}; - -/** - * Perform the OP_SWAP operation. - * @throws {ScriptError} - */ - -Stack.prototype.swap = function swap() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_SWAP); - - this._swap(-2, -1); -}; - -/** - * Perform the OP_TUCK operation. - * @throws {ScriptError} - */ - -Stack.prototype.tuck = function tuck() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_TUCK); - - this.splice(this.length - 2, 0, this.top(-1)); -}; - -/** - * Perform the OP_2DROP operation. - * @throws {ScriptError} - */ - -Stack.prototype.drop2 = function drop2() { - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2DROP); - - this.pop(); - this.pop(); -}; - -/** - * Perform the OP_2DUP operation. - * @throws {ScriptError} - */ - -Stack.prototype.dup2 = function dup2() { - var v1, v2; - - if (this.length < 2) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2DUP); - - v1 = this.top(-2); - v2 = this.top(-1); - - this.push(v1); - this.push(v2); -}; - -/** - * Perform the OP_3DUP operation. - * @throws {ScriptError} - */ - -Stack.prototype.dup3 = function dup3() { - var v1, v2, v3; - - if (this.length < 3) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_3DUP); - - v1 = this.top(-3); - v2 = this.top(-2); - v3 = this.top(-1); - - this.push(v1); - this.push(v2); - this.push(v3); -}; - -/** - * Perform the OP_2OVER operation. - * @throws {ScriptError} - */ - -Stack.prototype.over2 = function over2() { - var v1, v2; - - if (this.length < 4) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2OVER); - - v1 = this.top(-4); - v2 = this.top(-3); - - this.push(v1); - this.push(v2); -}; - -/** - * Perform the OP_2ROT operation. - * @throws {ScriptError} - */ - -Stack.prototype.rot2 = function rot2() { - var v1, v2; - - if (this.length < 6) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2ROT); - - v1 = this.top(-6); - v2 = this.top(-5); - - this.splice(this.length - 6, 2); - this.push(v1); - this.push(v2); -}; - -/** - * Perform the OP_2SWAP operation. - * @throws {ScriptError} - */ - -Stack.prototype.swap2 = function swap2() { - if (this.length < 4) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_2SWAP); - - this._swap(-4, -2); - this._swap(-3, -1); -}; - -/** - * Perform the OP_SIZE operation. - * @throws {ScriptError} - */ - -Stack.prototype.size = function size() { - if (this.length < 1) - throw new ScriptError('INVALID_STACK_OPERATION', opcodes.OP_SIZE); - - this.push(bcoin.script.array(this.top(-1).length)); -}; - /** * Test an object to see if it is a Stack. * @param {Object} obj @@ -527,7 +280,7 @@ Stack.prototype.size = function size() { */ Stack.isStack = function isStack(obj) { - return obj && Array.isArray(obj.items) && typeof obj.swap2 === 'function'; + return obj && Array.isArray(obj.items) && typeof obj.swap === 'function'; }; /* diff --git a/lib/script/witness.js b/lib/script/witness.js index 5f3f6722..8b096ef8 100644 --- a/lib/script/witness.js +++ b/lib/script/witness.js @@ -7,17 +7,22 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Witness; + var bn = require('bn.js'); -var constants = bcoin.constants; +var constants = require('../protocol/constants'); var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); var opcodes = constants.opcodes; -var STACK_FALSE = new Buffer([]); +var STACK_FALSE = new Buffer(0); var STACK_NEGATE = new Buffer([0x81]); var scriptTypes = constants.scriptTypes; var Script = require('./script'); var Opcode = require('./opcode'); +var BufferWriter = require('../utils/writer'); +var BufferReader = require('../utils/reader'); +var Address = require('../primitives/address'); +var Stack = require('./stack'); /** * Refers to the witness field of segregated witness transactions. @@ -159,7 +164,7 @@ Witness.prototype.clone = function clone() { */ Witness.prototype.toStack = function toStack() { - return new bcoin.stack(this.items.slice()); + return new Stack(this.items.slice()); }; /** @@ -185,7 +190,7 @@ Witness.prototype.getInputType = function getInputType() { */ Witness.prototype.getInputAddress = function getInputAddress() { - return bcoin.address.fromWitness(this); + return Address.fromWitness(this); }; /** @@ -301,7 +306,7 @@ Witness.prototype.indexOf = function indexOf(data) { */ Witness.prototype.toRaw = function toRaw(writer) { - var p = bcoin.writer(writer); + var p = BufferWriter(writer); var i; p.writeVarint(this.items.length); @@ -515,7 +520,7 @@ Witness.encodeItem = function encodeItem(data) { */ Witness.prototype.fromRaw = function fromRaw(data) { - var p = bcoin.reader(data); + var p = BufferReader(data); var chunkCount = p.readVarint(); var i; diff --git a/lib/utils/async.js b/lib/utils/async.js index 51b96df8..0fd09abb 100644 --- a/lib/utils/async.js +++ b/lib/utils/async.js @@ -7,7 +7,8 @@ 'use strict'; var utils = require('../utils/utils'); -var assert = utils.assert; +var co = require('../utils/co'); +var assert = require('assert'); var EventEmitter = require('events').EventEmitter; /** @@ -34,97 +35,102 @@ utils.inherits(AsyncObject, EventEmitter); /** * Open the object (recallable). - * @param {Function} callback + * @returns {Promise} */ -AsyncObject.prototype.open = function open(callback) { - var self = this; - - callback = utils.ensure(callback); +AsyncObject.prototype.open = co(function* open() { + var err, unlock; assert(!this.closing, 'Cannot open while closing.'); if (this.loaded) - return utils.nextTick(callback); + return yield co.wait(); if (this.loading) - return this.once('open', callback); + return yield this._onOpen(); - if (this.locker) { - callback = this.locker.lock(open, [callback]); - assert(callback, 'Cannot call methods before load.'); - } + if (this.locker) + unlock = yield this.locker.lock(); this.emit('preopen'); this.loading = true; - this._open(function(err) { - utils.nextTick(function() { - if (err) { - self.loading = false; - self._error('open', err); - return callback(err); - } + try { + yield this._open(); + } catch (e) { + err = e; + } - self.loading = false; - self.loaded = true; - self.emit('open'); + yield co.wait(); - callback(); - }); - }); -}; + if (err) { + this.loading = false; + this._error('open', err); + if (unlock) + unlock(); + throw err; + } + + this.loading = false; + this.loaded = true; + this.emit('open'); + + if (unlock) + unlock(); +}); /** * Close the object (recallable). - * @param {Function} callback + * @returns {Promise} */ -AsyncObject.prototype.close = function close(callback) { - var self = this; - - callback = utils.ensure(callback); +AsyncObject.prototype.close = co(function* close() { + var unlock, err; assert(!this.loading, 'Cannot close while loading.'); if (!this.loaded) - return utils.nextTick(callback); + return yield co.wait(); if (this.closing) - return this.on('close', callback); + return yield this._onClose(); - if (this.locker) { - callback = this.locker.lock(close, [callback]); - if (!callback) - return; - } + if (this.locker) + unlock = yield this.locker.lock(); this.emit('preclose'); this.closing = true; this.loaded = false; - this._close(function(err) { - utils.nextTick(function() { - if (err) { - self.closing = false; - self._error('close', err); - return callback(err); - } + try { + yield this._close(); + } catch (e) { + err = e; + } - self.closing = false; - self.emit('close'); + yield co.wait(); - callback(); - }); - }); -}; + if (err) { + this.closing = false; + this._error('close', err); + if (unlock) + unlock(); + throw err; + } + + this.closing = false; + this.emit('close'); + + if (unlock) + unlock(); +}); /** * Close the object (recallable). * @method - * @param {Function} callback + * @returns {Promise} */ AsyncObject.prototype.destroy = AsyncObject.prototype.close; @@ -151,7 +157,7 @@ AsyncObject.prototype._error = function _error(event, err) { /** * Initialize the object. * @private - * @param {Function} callback + * @returns {Promise} */ AsyncObject.prototype._open = function _open(callback) { @@ -161,13 +167,39 @@ AsyncObject.prototype._open = function _open(callback) { /** * Close the object. * @private - * @param {Function} callback + * @returns {Promise} */ AsyncObject.prototype._close = function _close(callback) { throw new Error('Abstract method.'); }; +/** + * Wait for open event. + * @private + * @returns {Promise} + */ + +AsyncObject.prototype._onOpen = function _onOpen() { + var self = this; + return new Promise(function(resolve, reject) { + return self.once('open', resolve); + }); +}; + +/** + * Wait for close event. + * @private + * @returns {Promise} + */ + +AsyncObject.prototype._onClose = function _onClose() { + var self = this; + return new Promise(function(resolve, reject) { + return self.once('close', resolve); + }); +}; + /* * Expose */ diff --git a/lib/utils/base58.js b/lib/utils/base58.js new file mode 100644 index 00000000..ccfbad2b --- /dev/null +++ b/lib/utils/base58.js @@ -0,0 +1,132 @@ +/*! + * base58.js - base58 for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var native = require('./native'); +var assert = require('assert'); + +/* + * Base58 + */ + +var base58 = '' + + '123456789' + + 'ABCDEFGHJKLMNPQRSTUVWXYZ' + + 'abcdefghijkmnopqrstuvwxyz'; + +var unbase58 = {}; + +for (var i = 0; i < base58.length; i++) + unbase58[base58[i]] = i; + +/** + * Encode a base58 string. + * @see https://github.com/bitcoin/bitcoin/blob/master/src/base58.cpp + * @param {Buffer} data + * @returns {Base58String} + */ + +exports.toBase58 = function toBase58(data) { + var zeroes = 0; + var length = 0; + var str = ''; + var i, b58, carry, j, k; + + for (i = 0; i < data.length; i++) { + if (data[i] !== 0) + break; + zeroes++; + } + + b58 = new Buffer(((data.length * 138 / 100) | 0) + 1); + b58.fill(0); + + for (; i < data.length; i++) { + carry = data[i]; + j = 0; + for (k = b58.length - 1; k >= 0; k--, j++) { + if (carry === 0 && j >= length) + break; + carry += 256 * b58[k]; + b58[k] = carry % 58; + carry = carry / 58 | 0; + } + assert(carry === 0); + length = j; + } + + i = b58.length - length; + while (i < b58.length && b58[i] === 0) + i++; + + for (j = 0; j < zeroes; j++) + str += '1'; + + for (; i < b58.length; i++) + str += base58[b58[i]]; + + return str; +}; + +if (native) + exports.toBase58 = native.toBase58; + +/** + * Decode a base58 string. + * @see https://github.com/bitcoin/bitcoin/blob/master/src/base58.cpp + * @param {Base58String} str + * @returns {Buffer} + * @throws on non-base58 character. + */ + +exports.fromBase58 = function fromBase58(str) { + var zeroes = 0; + var i = 0; + var b256, ch, carry, j, out; + + for (i = 0; i < str.length; i++) { + if (str[i] !== '1') + break; + zeroes++; + } + + b256 = new Buffer(((str.length * 733) / 1000 | 0) + 1); + b256.fill(0); + + for (; i < str.length; i++) { + ch = unbase58[str[i]]; + if (ch == null) + throw new Error('Non-base58 character.'); + + carry = ch; + for (j = b256.length - 1; j >= 0; j--) { + carry += 58 * b256[j]; + b256[j] = carry % 256; + carry = carry / 256 | 0; + } + + assert(carry === 0); + } + + i = 0; + while (i < b256.length && b256[i] === 0) + i++; + + out = new Buffer(zeroes + (b256.length - i)); + + for (j = 0; j < zeroes; j++) + out[j] = 0; + + while (i < b256.length) + out[j++] = b256[i++]; + + return out; +}; + +if (native) + exports.fromBase58 = native.fromBase58; diff --git a/lib/utils/bloom.js b/lib/utils/bloom.js index b225ad7f..d36627f8 100644 --- a/lib/utils/bloom.js +++ b/lib/utils/bloom.js @@ -374,6 +374,6 @@ function write(data, value, off) { exports = Bloom; exports.murmur3 = murmur3; -exports.rolling = RollingFilter; +exports.Rolling = RollingFilter; module.exports = exports; diff --git a/lib/utils/co.js b/lib/utils/co.js new file mode 100644 index 00000000..84f3f767 --- /dev/null +++ b/lib/utils/co.js @@ -0,0 +1,327 @@ +/*! + * co.js - promise and generator control flow for bcoin + * Originally based on yoursnetwork's "asink" module. + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var utils = require('./utils'); +var every; + +/** + * Execute an instantiated generator. + * @param {Generator} gen + * @returns {Promise} + */ + +function exec(gen) { + return new Promise(function(resolve, reject) { + function step(value, rejection) { + var next; + + try { + if (rejection) + next = gen.throw(value); + else + next = gen.next(value); + } catch (e) { + reject(e); + return; + } + + if (next.done) { + resolve(next.value); + return; + } + + if (!isPromise(next.value)) { + step(next.value, false); + return; + } + + next.value.then(succeed, fail); + } + + function succeed(value) { + step(value, false); + } + + function fail(value) { + step(value, true); + } + + step(undefined, false); + }); +} + +/** + * Execute generator function + * with a context and execute. + * @param {GeneratorFunction} generator + * @param {Object} ctx + * @returns {Promise} + */ + +function spawn(generator, ctx) { + var gen = generator.call(ctx); + return exec(gen); +} + +/** + * Wrap a generator function to be + * executed into a function that + * returns a promise. + * @param {GeneratorFunction} + * @returns {Function} + */ + +function co(generator) { + return function() { + var gen = generator.apply(this, arguments); + return exec(gen); + }; +} + +/** + * Test whether an object is a promise. + * @param {Object} obj + * @returns {Boolean} + */ + +function isPromise(obj) { + return obj && typeof obj.then === 'function'; +} + +/** + * Wrap a generator function to be + * executed into a function that + * accepts a node.js style callback. + * @param {GeneratorFunction} + * @returns {Function} + */ + +function cob(generator) { + return function(_) { + var i, args, callback, gen; + + if (arguments.length === 0 + || typeof arguments[arguments.length - 1] !== 'function') { + throw new Error((generator.name || 'Function') + ' requires a callback.'); + } + + args = new Array(arguments.length - 1); + callback = arguments[arguments.length - 1]; + + for (i = 0; i < args.length; i++) + args[i] = arguments[i]; + + gen = generator.apply(this, args); + + return cb(exec(gen), callback); + }; +} + +/** + * Wrap a generator function to be + * executed into a function that + * accepts a node.js style callback. + * Only executes callback on error. + * @param {GeneratorFunction} + * @returns {Function} + */ + +function con(generator) { + return function() { + var i, args, callback, gen; + + if (arguments.length === 0 + || typeof arguments[arguments.length - 1] !== 'function') { + throw new Error((generator.name || 'Function') + ' requires a callback.'); + } + + args = new Array(arguments.length); + callback = arguments[arguments.length - 1]; + + for (i = 0; i < args.length; i++) + args[i] = arguments[i]; + + gen = generator.apply(this, args); + + return exec(gen).catch(function(err) { + // Escape the promise's scope: + utils.nextTick(function() { + callback(err); + }); + }); + }; +} + +/** + * Wait for promise to resolve and + * execute a node.js style callback. + * @param {Promise} promise + * @returns {Promise} + */ + +function cb(promise, callback) { + promise.then(function(value) { + utils.nextTick(function() { + callback(null, value); + }); + }, function(err) { + utils.nextTick(function() { + callback(err); + }); + }); +} + +/** + * Wait for a nextTick with a promise. + * @returns {Promise} + */ + +function wait() { + return new Promise(function(resolve, reject) { + utils.nextTick(resolve); + }); +}; + +/** + * Wait for a timeout with a promise. + * @param {Number} time + * @returns {Promise} + */ + +function timeout(time) { + return new Promise(function(resolve, reject) { + setTimeout(resolve, time); + }); +} + +/** + * Wrap `resolve` and `reject` into + * a node.js style callback. + * @param {Function} resolve + * @param {Function} reject + * @returns {Function} + */ + +function wrap(resolve, reject) { + return function(err, result) { + if (err) { + reject(err); + return; + } + resolve(result); + }; +} + +/** + * Call a function that accepts node.js + * style callbacks, wrap with a promise. + * @private + * @param {Object} ctx + * @param {Function} func + * @param {Array} args + * @returns {Promise} + */ + +function _call(ctx, func, args) { + return new Promise(function(resolve, reject) { + args.push(wrap(resolve, reject)); + func.apply(ctx, args); + }); +} + +/** + * Call a function that accepts node.js + * style callbacks, wrap with a promise. + * @param {Function} func + * @returns {Promise} + */ + +function call(func) { + var args = new Array(Math.max(0, arguments.length - 1)); + var i; + + for (i = 1; i < arguments.length; i++) + args[i - 1] = arguments[i]; + + return _call(this, func, args); +} + +/** + * Wrap a function that accepts node.js + * style callbacks into a function that + * returns a promise. + * @param {Function} func + * @param {Object?} ctx + * @returns {Function} + */ + +function promisify(func, ctx) { + return function() { + var args = new Array(arguments.length); + var i; + + for (i = 0; i < arguments.length; i++) + args[i] = arguments[i]; + + return _call(ctx || this, func, args); + }; +} + +/** + * Execute each promise and + * have them pass a truth test. + * @param {Promise[]} jobs + * @returns {Promise} + */ + +every = co(function* every(jobs) { + var result = yield Promise.all(jobs); + var i; + + for (i = 0; i < result.length; i++) { + if (!result[i]) + return false; + } + + return true; +}); + +/* + * This drives me nuts. + */ + +if (typeof window !== 'undefined') { + window.onunhandledrejection = function(event) { + throw event.reason; + }; +} else { + process.on('unhandledRejection', function(err, promise) { + throw err; + }); +} + +/* + * Expose + */ + +exports = co; +exports.exec = exec; +exports.spawn = spawn; +exports.co = co; +exports.cob = cob; +exports.con = con; +exports.cb = cb; +exports.wait = wait; +exports.timeout = timeout; +exports.wrap = wrap; +exports.call = call; +exports.promisify = promisify; +exports.every = every; + +module.exports = exports; diff --git a/lib/utils/errors.js b/lib/utils/errors.js index f8067eb0..fcaaa41b 100644 --- a/lib/utils/errors.js +++ b/lib/utils/errors.js @@ -7,9 +7,8 @@ 'use strict'; -var bcoin = require('../env'); -var utils = bcoin.utils; -var constants = bcoin.constants; +var utils = require('../utils/utils'); +var constants = require('../protocol/constants'); /** * An error thrown during verification. Can be either diff --git a/lib/utils/index.js b/lib/utils/index.js new file mode 100644 index 00000000..e5f268a6 --- /dev/null +++ b/lib/utils/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./utils'); diff --git a/lib/utils/lazy.js b/lib/utils/lazy.js new file mode 100644 index 00000000..27d6c3e4 --- /dev/null +++ b/lib/utils/lazy.js @@ -0,0 +1,18 @@ +/*! + * lazy.js - lazy loading for bcoin + * Copyright (c) 2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +module.exports = function lazy(require, exports) { + return function _require(name, path) { + var cache; + exports.__defineGetter__(name, function() { + if (!cache) + cache = require(path); + return cache; + }); + }; +}; diff --git a/lib/utils/locker.js b/lib/utils/locker.js index 9850e8c6..c65c9f1b 100644 --- a/lib/utils/locker.js +++ b/lib/utils/locker.js @@ -9,44 +9,44 @@ var EventEmitter = require('events').EventEmitter; var utils = require('../utils/utils'); -var assert = utils.assert; +var assert = require('assert'); /** * Represents a mutex lock for locking asynchronous object methods. * @exports Locker * @constructor - * @param {Function} parent - Parent object. * @param {Function?} add - `add` method (whichever method is queuing data). */ -function Locker(parent, add) { +function Locker(add) { if (!(this instanceof Locker)) - return Locker.create(parent, add); + return Locker.create(add); EventEmitter.call(this); - this.parent = parent; + this.add = add === true; + this.jobs = []; this.busy = false; this.pending = []; this.pendingMap = {}; - this.add = add; + + this.unlocker = this.unlock.bind(this); } utils.inherits(Locker, EventEmitter); /** * Create a closure scoped locker. - * @param {Function} parent - Parent object. * @param {Function?} add * @returns {Function} Lock method. */ -Locker.create = function create(parent, add) { - var locker = new Locker(parent, add); - return function lock(func, args, force) { - return locker.lock(func, args, force); +Locker.create = function create(add) { + var locker = new Locker(add); + return function lock(arg1, arg2) { + return locker.lock(arg1, arg2); }; }; @@ -64,73 +64,71 @@ Locker.prototype.hasPending = function hasPending(key) { /** * Lock the parent object and all its methods * which use the locker. Begin to queue calls. - * @param {Function} func - The method being called. - * @param {Array} args - Arguments passed to the method. - * @param {Boolean?} force - Force a call. - * @returns {Function} Unlocker - must be + * @param {Boolean?} force - Bypass the lock. + * @returns {Promise->Function} Unlocker - must be * called once the method finishes executing in order * to resolve the queue. */ -Locker.prototype.lock = function lock(func, args, force) { +Locker.prototype.lock = function lock(arg1, arg2) { var self = this; - var callback = args[args.length - 1]; - var obj, called; + var force, object; - if (typeof callback !== 'function') - throw new Error(func.name + ' requires a callback.'); + if (this.add) { + object = arg1; + force = arg2; + } else { + force = arg1; + } if (force) { assert(this.busy); - return function unlock(err, res1, res2) { - assert(!called, 'Locked callback executed twice.'); - called = true; - callback(err, res1, res2); - }; + return Promise.resolve(utils.nop); } if (this.busy) { - if (this.add && func === this.add) { - obj = args[0]; - this.pending.push(obj); - this.pendingMap[obj.hash('hex')] = true; + if (object) { + this.pending.push(object); + this.pendingMap[object.hash('hex')] = true; } - this.jobs.push([func, args]); - return; + return new Promise(function(resolve, reject) { + self.jobs.push([resolve, object]); + }); } this.busy = true; - return function unlock(err, res1, res2) { - var item, obj; + return Promise.resolve(this.unlocker); +}; - assert(!called, 'Locked callback executed twice.'); - called = true; +/** + * The actual unlock callback. + * @private + */ - self.busy = false; +Locker.prototype.unlock = function unlock() { + var item, resolve, object; - if (self.add && func === self.add) { - if (self.pending.length === 0) - self.emit('drain'); - } + this.busy = false; - if (self.jobs.length === 0) { - callback(err, res1, res2); - return; - } + if (this.pending.length === 0) + this.emit('drain'); - item = self.jobs.shift(); + if (this.jobs.length === 0) + return; - if (self.add && item[0] === self.add) { - obj = item[1][0]; - assert(obj === self.pending.shift()); - delete self.pendingMap[obj.hash('hex')]; - } + item = this.jobs.shift(); + resolve = item[0]; + object = item[1]; - item[0].apply(self.parent, item[1]); + if (object) { + assert(object === this.pending.shift()); + delete this.pendingMap[object.hash('hex')]; + } - callback(err, res1, res2); - }; + this.busy = true; + + resolve(this.unlocker); }; /** @@ -138,25 +136,28 @@ Locker.prototype.lock = function lock(func, args, force) { */ Locker.prototype.destroy = function destroy() { - if (this.add) { - this.pending.length = 0; - this.pendingMap = {}; - } this.jobs.length = 0; + this.busy = false; + this.pending.length = 0; + this.pendingMap = {}; }; /** * Wait for a drain (empty queue). - * @param {Function} callback + * @returns {Promise} */ -Locker.prototype.onDrain = function onDrain(callback) { +Locker.prototype.onDrain = function onDrain() { + var self = this; + assert(this.add, 'Cannot wait for drain without add method.'); - if (this.pending.length === 0) - return callback(); + return new Promise(function(resolve, reject) { + if (self.pending.length === 0) + return resolve(); - this.once('drain', callback); + self.once('drain', resolve); + }); }; /** @@ -164,28 +165,25 @@ Locker.prototype.onDrain = function onDrain(callback) { * Locks methods according to passed-in key. * @exports MappedLock * @constructor - * @param {Function} parent - Parent object. */ -function MappedLock(parent) { +function MappedLock() { if (!(this instanceof MappedLock)) - return MappedLock.create(parent); + return MappedLock.create(); - this.parent = parent; - this.jobs = []; + this.jobs = {}; this.busy = {}; } /** * Create a closure scoped locker. - * @param {Function} parent - Parent object. * @returns {Function} Lock method. */ -MappedLock.create = function create(parent) { - var locker = new MappedLock(parent); - return function lock(key, func, args, force) { - return locker.lock(key, func, args, force); +MappedLock.create = function create() { + var locker = new MappedLock(); + return function lock(key, force) { + return locker.lock(key, force); }; }; @@ -202,55 +200,71 @@ MappedLock.create = function create(parent) { * to resolve the queue. */ -MappedLock.prototype.lock = function lock(key, func, args, force) { +MappedLock.prototype.lock = function lock(key, force) { var self = this; - var callback = args[args.length - 1]; - var called; - - if (typeof callback !== 'function') - throw new Error(func.name + ' requires a callback.'); if (force || key == null) { assert(key == null || this.busy[key]); - return function unlock(err, res1, res2) { - assert(!called, 'Locked callback executed twice.'); - called = true; - callback(err, res1, res2); - }; + return Promise.resolve(utils.nop); } if (this.busy[key]) { - this.jobs.push([func, args]); - return; + return new Promise(function(resolve, reject) { + if (!self.jobs[key]) + self.jobs[key] = []; + self.jobs[key].push(resolve); + }); } this.busy[key] = true; - return function unlock(err, res1, res2) { - var item; + return Promise.resolve(this.unlock(key)); +}; - assert(!called, 'Locked callback executed twice.'); - called = true; +/** + * Create an unlock callback. + * @private + * @param {String} key + * @returns {Function} Unlocker. + */ + +MappedLock.prototype.unlock = function unlock(key) { + var self = this; + return function unlocker() { + var jobs = self.jobs[key]; + var resolve; delete self.busy[key]; - if (self.jobs.length === 0) { - callback(err, res1, res2); + if (!jobs) return; - } - item = self.jobs.shift(); + resolve = jobs.shift(); + assert(resolve); - item[0].apply(self.parent, item[1]); + if (jobs.length === 0) + delete self.jobs[key]; - callback(err, res1, res2); + self.busy[key] = true; + + resolve(unlocker); }; }; +/** + * Destroy the locker. Purge all pending calls. + */ + +MappedLock.prototype.destroy = function destroy() { + this.jobs = {}; + this.busy = {}; +}; + /* * Expose */ exports = Locker; -exports.mapped = MappedLock; +exports.Mapped = MappedLock; + module.exports = exports; diff --git a/lib/utils/lru.js b/lib/utils/lru.js index b699d10b..82065f74 100644 --- a/lib/utils/lru.js +++ b/lib/utils/lru.js @@ -295,6 +295,21 @@ LRU.prototype.keys = function keys() { return keys; }; +/** + * Collect all values in the cache, sorted by LRU. + * @returns {String[]} + */ + +LRU.prototype.values = function values() { + var values = []; + var item; + + for (item = this.head; item; item = item.next) + values.push(item.value); + + return values; +}; + /** * Convert the LRU cache to an array of items. * @returns {Object[]} @@ -336,12 +351,16 @@ function NullCache(size) {} NullCache.prototype.set = function set(key, value) {}; NullCache.prototype.remove = function remove(key) {}; NullCache.prototype.get = function get(key) {}; -NullCache.prototype.has = function has(key) {}; +NullCache.prototype.has = function has(key) { return false; }; NullCache.prototype.reset = function reset() {}; +NullCache.prototype.keys = function keys(key) { return []; }; +NullCache.prototype.values = function values(key) { return []; }; +NullCache.prototype.toArray = function toArray(key) { return []; }; /* * Expose */ -LRU.nil = NullCache; +LRU.Nil = NullCache; + module.exports = LRU; diff --git a/lib/utils/reader.js b/lib/utils/reader.js index 0d7ec221..52ded3d7 100644 --- a/lib/utils/reader.js +++ b/lib/utils/reader.js @@ -9,7 +9,7 @@ var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; +var assert = require('assert'); /** * An object that allows reading of buffers in a sane manner. diff --git a/lib/utils/uri.js b/lib/utils/uri.js index 284a1bc8..0459c79a 100644 --- a/lib/utils/uri.js +++ b/lib/utils/uri.js @@ -6,15 +6,15 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); -var assert = utils.assert; +var Address = require('../primitives/address'); +var assert = require('assert'); function URI(options) { if (!(this instanceof URI)) return new URI(options); - this.address = new bcoin.address(); + this.address = new Address(); this.amount = -1; this.label = null; this.message = null; @@ -46,9 +46,9 @@ URI.prototype.fromOptions = function fromOptions(options) { this.message = options.message; } - if (options.r) { + if (options.request) { assert(typeof options.request === 'string'); - this.request = options.r; + this.request = options.request; } return this; diff --git a/lib/utils/utils.js b/lib/utils/utils.js index 474ad6dd..b27ba3ac 100644 --- a/lib/utils/utils.js +++ b/lib/utils/utils.js @@ -16,11 +16,11 @@ var utils = exports; var assert = require('assert'); -var native = require('./native'); +var base58 = require('./base58'); var bn = require('bn.js'); var util = require('util'); var Number, Math, Date; -var fs; +var fs, lazy; /** * Reference to the global object. @@ -103,126 +103,40 @@ utils.copy = function copy(data) { return clone; }; -/* - * Base58 +/** + * Concatenate two buffers. + * @param {Buffer} a + * @param {Buffer} b + * @returns {Buffer} */ -var base58 = '' - + '123456789' - + 'ABCDEFGHJKLMNPQRSTUVWXYZ' - + 'abcdefghijkmnopqrstuvwxyz'; - -var unbase58 = {}; - -for (var i = 0; i < base58.length; i++) - unbase58[base58[i]] = i; +utils.concat = function concat(a, b) { + var data = new Buffer(a.length + b.length); + a.copy(data, 0); + b.copy(data, a.length); + return data; +}; /** * Encode a base58 string. * @see https://github.com/bitcoin/bitcoin/blob/master/src/base58.cpp + * @function * @param {Buffer} data * @returns {Base58String} */ -utils.toBase58 = function toBase58(data) { - var zeroes = 0; - var length = 0; - var str = ''; - var i, b58, carry, j, k; - - for (i = 0; i < data.length; i++) { - if (data[i] !== 0) - break; - zeroes++; - } - - b58 = new Buffer(((data.length * 138 / 100) | 0) + 1); - b58.fill(0); - - for (; i < data.length; i++) { - carry = data[i]; - j = 0; - for (k = b58.length - 1; k >= 0; k--, j++) { - if (carry === 0 && j >= length) - break; - carry += 256 * b58[k]; - b58[k] = carry % 58; - carry = carry / 58 | 0; - } - assert(carry === 0); - length = j; - } - - i = b58.length - length; - while (i < b58.length && b58[i] === 0) - i++; - - for (j = 0; j < zeroes; j++) - str += '1'; - - for (; i < b58.length; i++) - str += base58[b58[i]]; - - return str; -}; - -if (native) - utils.toBase58 = native.toBase58; +utils.toBase58 = base58.toBase58; /** * Decode a base58 string. * @see https://github.com/bitcoin/bitcoin/blob/master/src/base58.cpp + * @function * @param {Base58String} str * @returns {Buffer} * @throws on non-base58 character. */ -utils.fromBase58 = function fromBase58(str) { - var zeroes = 0; - var i = 0; - var b256, ch, carry, j, out; - - for (i = 0; i < str.length; i++) { - if (str[i] !== '1') - break; - zeroes++; - } - - b256 = new Buffer(((str.length * 733) / 1000 | 0) + 1); - b256.fill(0); - - for (; i < str.length; i++) { - ch = unbase58[str[i]]; - if (ch == null) - throw new Error('Non-base58 character.'); - - carry = ch; - for (j = b256.length - 1; j >= 0; j--) { - carry += 58 * b256[j]; - b256[j] = carry % 256; - carry = carry / 256 | 0; - } - - assert(carry === 0); - } - - i = 0; - while (i < b256.length && b256[i] === 0) - i++; - - out = new Buffer(zeroes + (b256.length - i)); - - for (j = 0; j < zeroes; j++) - out[j] = 0; - - while (i < b256.length) - out[j++] = b256[i++]; - - return out; -}; - -if (native) - utils.fromBase58 = native.fromBase58; +utils.fromBase58 = base58.fromBase58; /** * Test whether a string is base58 (note that you @@ -310,7 +224,7 @@ utils.equal = function equal(a, b) { * or `setInterval` depending. * @name nextTick * @function - * @param {Function} callback + * @returns {Promise} */ if (utils.isBrowser) @@ -326,43 +240,6 @@ if (typeof setImmediate === 'function') { }; } -/** - * Wrap a function in a `nextTick`. - * @param {Function} callback - * @returns {Function} Asyncified function. - */ - -utils.asyncify = function asyncify(callback) { - if (callback && callback._asyncified) - return callback; - - function asyncifyFn(err, result1, result2) { - if (!callback) - return; - utils.nextTick(function() { - callback(err, result1, result2); - }); - } - - asyncifyFn._asyncified = true; - if (callback) - asyncifyFn._once = callback._once; - - return asyncifyFn; -}; - -/** - * Ensure a callback exists, return a NOP if not. - * @param {Function} callback - * @returns {Function} - */ - -utils.ensure = function ensure(callback) { - if (!callback) - return utils.nop; - return callback; -}; - /** * Reverse a hex-string (used because of * bitcoind's affinity for uint256le). @@ -407,15 +284,6 @@ utils.merge = function merge(target) { if (Object.assign) utils.merge = Object.assign; -/** - * Assertion. - * @function - * @param {Boolean} value - Expression. - * @param {String?} message - Optional error message. - */ - -utils.assert = assert; - /** * Safely convert satoshis to a BTC string. * This function explicitly avoids any @@ -1634,199 +1502,6 @@ utils.icmp = function icmp(target, data, start) { return 0; }; -/** - * Asnchronously iterate over a range in parallel. - * @param {Number} from - * @param {Number} to - * @param {Function} iter - * @param {Function} callback - */ - -utils.forRange = function forRange(from, to, iter, callback) { - var pending = to - from; - var i, error; - - callback = utils.asyncify(callback); - - if (pending <= 0) - return callback(); - - function next(err) { - assert(pending > 0); - if (err) - error = err; - if (!--pending) - callback(error); - } - - for (i = from; i < to; i++) - iter(i, next, i); -}; - -/** - * Asynchronously iterate over an array in parallel. - * @param {Array} obj - * @param {Function} iter - * @param {Function} callback - */ - -utils.forEach = function forEach(obj, iter, callback) { - var pending = obj.length; - var error; - - callback = utils.asyncify(callback); - - if (!pending) - return callback(); - - function next(err) { - assert(pending > 0); - if (err) - error = err; - if (!--pending) - callback(error); - } - - obj.forEach(function(item, i) { - iter(item, next, i); - }); -}; - -/** - * Asnchronously iterate over a range in serial. - * @param {Number} from - * @param {Number} to - * @param {Function} iter - * @param {Function} callback - */ - -utils.forRangeSerial = function forRangeSerial(from, to, iter, callback) { - var called = false; - - callback = utils.ensure(callback); - - (function next(err) { - assert(!called); - if (err) { - called = true; - return callback(err); - } - if (from >= to) { - called = true; - return callback(); - } - from++; - utils.nextTick(function() { - iter(from - 1, next, from - 1); - }); - })(); -}; - -/** - * Asynchronously iterate over an array in serial. - * @param {Array} obj - * @param {Function} iter - * @param {Function} callback - */ - -utils.forEachSerial = function forEachSerial(obj, iter, callback) { - var i = 0; - var called = false; - - callback = utils.ensure(callback); - - (function next(err) { - var item; - assert(!called); - if (err) { - called = true; - return callback(err); - } - if (i >= obj.length) { - called = true; - return callback(); - } - item = obj[i]; - i++; - utils.nextTick(function() { - iter(item, next, i - 1); - }); - })(); -}; - -/** - * Asynchronously apply a truth test to every - * member of an array in parallel. - * @param {Array} obj - * @param {Function} iter - * @param {Function} callback - */ - -utils.every = function every(obj, iter, callback) { - var pending = obj.length; - var result = true; - var error; - - callback = utils.asyncify(callback); - - if (!pending) - return callback(null, result); - - function next(err, res) { - assert(pending > 0); - if (err) - error = err; - if (!res) - result = false; - if (!--pending) { - if (error) - return callback(error); - callback(null, result); - } - } - - obj.forEach(function(item, i) { - iter(item, next, i); - }); -}; - -/** - * Asynchronously apply a truth test to every - * member of an array in serial. - * @param {Array} obj - * @param {Function} iter - * @param {Function} callback - */ - -utils.everySerial = function everySerial(obj, iter, callback) { - var i = 0; - var called = false; - - callback = utils.ensure(callback); - - (function next(err, res) { - var item; - assert(!called); - if (err) { - called = true; - return callback(err); - } - if (!res) { - called = true; - return callback(null, false); - } - if (i >= obj.length) { - called = true; - return callback(null, true); - } - item = obj[i]; - i++; - utils.nextTick(function() { - iter(item, next, i - 1); - }); - })(null, true); -}; - /** * Convert bytes to mb. * @param {Number} size @@ -1873,33 +1548,6 @@ utils.inherits = function inherits(obj, from) { obj.prototype.constructor = obj; }; -/** - * Wrap a callback to ensure it is only called once. - * @param {Function} callback - * @returns {Function} Wrapped callback. - */ - -utils.once = function once(callback) { - var called; - - if (callback && callback._once) - return callback; - - function onceFn(err, result1, result2) { - if (called) - return; - called = true; - if (callback) - callback(err, result1, result2); - } - - onceFn._once = true; - if (callback) - onceFn._asyncified = callback._asyncified; - - return onceFn; -}; - /** * Find index of a buffer in an array of buffers. * @param {Buffer[]} obj @@ -1991,96 +1639,6 @@ utils.hex32 = function hex32(num) { } }; -/** - * Wrap a callback with an `unlock` callback. - * @see Locker - * @param {Function} callback - * @param {Function} unlock - * @returns {Function} Wrapped callback. - */ - -utils.wrap = function wrap(callback, unlock) { - return function(err, res1, res2) { - unlock(); - if (callback) - callback(err, res1, res2); - }; -}; - -/** - * Execute a stack of functions in parallel. - * @param {Function[]} stack - * @param {Function} callback - */ - -utils.parallel = function parallel(stack, callback) { - var pending = stack.length; - var error; - var i; - - callback = utils.once(callback); - - if (!pending) - return utils.nextTick(callback); - - function next(err) { - assert(pending > 0); - if (err) - error = err; - if (!--pending) - callback(error); - } - - for (i = 0; i < stack.length; i++) { - try { - // if (stack[i].length >= 2) { - // stack[i](error, next); - // error = null; - // continue; - // } - if (error) - continue; - stack[i](next); - } catch (e) { - pending--; - error = e; - } - } -}; - -/** - * Execute a stack of functions in serial. - * @param {Function[]} stack - * @param {Function} callback - */ - -utils.serial = function serial(stack, callback) { - var i = 0; - (function next(err) { - var cb = stack[i++]; - - if (!cb) - return callback(err); - - // if (cb.length >= 2) { - // try { - // return cb(err, next); - // } catch (e) { - // return next(e); - // } - // } - - if (err) - return utils.nextTick(next.bind(null, err)); - - try { - return cb(next); - } catch (e) { - return next(e); - } - })(); -}; - /** * Convert an array to a map. * @param {String[]} obj @@ -2344,7 +1902,9 @@ utils.isName = function isName(key) { if (typeof key !== 'string') return false; - // Maximum worst case size: 80 bytes + if (!/^[\-\._0-9A-Za-z]+$/.test(key)) + return false; + return key.length >= 1 && key.length <= 40; }; @@ -2366,3 +1926,29 @@ utils.VerifyResult = function VerifyResult() { this.reason = 'unknown'; this.score = 0; }; + +/** + * Create a lazy loader. + * @param {Function} require + * @param {Object} exports + */ + +utils.lazy = require('./lazy'); + +/* + * Expose other objects. + */ + +lazy = utils.lazy(require, exports); + +lazy('AsyncObject', './async'); +lazy('bloom', './bloom'); +lazy('errors', './errors'); +lazy('ip', './ip'); +lazy('Locker', './locker'); +lazy('LRU', './lru'); +lazy('murmur3', './murmur3'); +lazy('co', './co'); +lazy('uri', './uri'); +lazy('BufferReader', './reader'); +lazy('BufferWriter', './writer'); diff --git a/lib/utils/writer.js b/lib/utils/writer.js index 381c1e9d..250e6660 100644 --- a/lib/utils/writer.js +++ b/lib/utils/writer.js @@ -9,7 +9,7 @@ var utils = require('../utils/utils'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; +var assert = require('assert'); /* * Constants diff --git a/lib/wallet/account.js b/lib/wallet/account.js index 3e3705a1..4e26680c 100644 --- a/lib/wallet/account.js +++ b/lib/wallet/account.js @@ -6,11 +6,15 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); -var assert = utils.assert; +var co = require('../utils/co'); +var assert = require('assert'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); +var Path = require('./path'); +var Script = require('../script/script'); +var WalletKey = require('./walletkey'); +var HD = require('../hd/hd'); /** * Represents a BIP44 Account belonging to a {@link Wallet}. @@ -44,24 +48,28 @@ function Account(db, options) { this.db = db; this.network = db.network; + this.wallet = null; this.lookahead = Account.MAX_LOOKAHEAD; - this.receiveAddress = null; - this.changeAddress = null; + this.receive = null; + this.change = null; + this.nested = null; this.wid = 0; this.id = null; this.name = null; + this.initialized = false; this.witness = this.db.options.witness; - this.accountKey = null; - this.accountIndex = 0; - this.receiveDepth = 0; - this.changeDepth = 0; + this.watchOnly = false; this.type = Account.types.PUBKEYHASH; this.m = 1; this.n = 1; + this.accountIndex = 0; + this.receiveDepth = 0; + this.changeDepth = 0; + this.nestedDepth = 0; + this.accountKey = null; this.keys = []; - this.initialized = false; if (options) this.fromOptions(options); @@ -100,7 +108,7 @@ Account.prototype.fromOptions = function fromOptions(options) { assert(options, 'Options are required.'); assert(utils.isNumber(options.wid)); assert(utils.isName(options.id), 'Bad Wallet ID.'); - assert(bcoin.hd.isHD(options.accountKey), 'Account key is required.'); + assert(HD.isHD(options.accountKey), 'Account key is required.'); assert(utils.isNumber(options.accountIndex), 'Account index is required.'); this.wid = options.wid; @@ -111,26 +119,19 @@ Account.prototype.fromOptions = function fromOptions(options) { this.name = options.name; } + if (options.initialized != null) { + assert(typeof options.initialized === 'boolean'); + this.initialized = options.initialized; + } + if (options.witness != null) { assert(typeof options.witness === 'boolean'); this.witness = options.witness; } - this.accountKey = options.accountKey; - - if (options.accountIndex != null) { - assert(utils.isNumber(options.accountIndex)); - this.accountIndex = options.accountIndex; - } - - if (options.receiveDepth != null) { - assert(utils.isNumber(options.receiveDepth)); - this.receiveDepth = options.receiveDepth; - } - - if (options.changeDepth != null) { - assert(utils.isNumber(options.changeDepth)); - this.changeDepth = options.changeDepth; + if (options.watchOnly != null) { + assert(typeof options.watchOnly === 'boolean'); + this.watchOnly = options.watchOnly; } if (options.type != null) { @@ -154,20 +155,37 @@ Account.prototype.fromOptions = function fromOptions(options) { this.n = options.n; } - if (options.initialized != null) { - assert(typeof options.initialized === 'boolean'); - this.initialized = options.initialized; + if (options.accountIndex != null) { + assert(utils.isNumber(options.accountIndex)); + this.accountIndex = options.accountIndex; } + if (options.receiveDepth != null) { + assert(utils.isNumber(options.receiveDepth)); + this.receiveDepth = options.receiveDepth; + } + + if (options.changeDepth != null) { + assert(utils.isNumber(options.changeDepth)); + this.changeDepth = options.changeDepth; + } + + if (options.nestedDepth != null) { + assert(utils.isNumber(options.nestedDepth)); + this.nestedDepth = options.nestedDepth; + } + + this.accountKey = options.accountKey; + if (this.n > 1) this.type = Account.types.MULTISIG; - if (this.m < 1 || this.m > this.n) - throw new Error('m ranges between 1 and n'); - if (!this.name) this.name = this.accountIndex + ''; + if (this.m < 1 || this.m > this.n) + throw new Error('m ranges between 1 and n'); + if (options.keys) { assert(Array.isArray(options.keys)); for (i = 0; i < options.keys.length; i++) @@ -193,44 +211,51 @@ Account.fromOptions = function fromOptions(db, options) { * @const {Number} */ -Account.MAX_LOOKAHEAD = 5; +Account.MAX_LOOKAHEAD = 10; /** * Attempt to intialize the account (generating * the first addresses along with the lookahead * addresses). Called automatically from the * walletdb. - * @param {Function} callback + * @returns {Promise} */ -Account.prototype.init = function init(callback) { +Account.prototype.init = co(function* init() { // Waiting for more keys. if (this.keys.length !== this.n - 1) { assert(!this.initialized); this.save(); - return callback(); + return; } assert(this.receiveDepth === 0); assert(this.changeDepth === 0); + assert(this.nestedDepth === 0); this.initialized = true; - this.setDepth(1, 1, callback); -}; + yield this.setDepth(1, 1, 1); +}); /** * Open the account (done after retrieval). - * @param {Function} callback + * @returns {Promise} */ -Account.prototype.open = function open(callback) { +Account.prototype.open = function open() { if (!this.initialized) - return callback(); + return Promise.resolve(null); - this.receiveAddress = this.deriveReceive(this.receiveDepth - 1); - this.changeAddress = this.deriveChange(this.changeDepth - 1); + if (this.receive) + return Promise.resolve(null); - callback(); + this.receive = this.deriveReceive(this.receiveDepth - 1); + this.change = this.deriveChange(this.changeDepth - 1); + + if (this.witness) + this.nested = this.deriveNested(this.nestedDepth - 1); + + return Promise.resolve(null); }; /** @@ -244,10 +269,10 @@ Account.prototype.open = function open(callback) { Account.prototype.pushKey = function pushKey(key) { var index; - if (bcoin.hd.isExtended(key)) - key = bcoin.hd.fromBase58(key); + if (HD.isExtended(key)) + key = HD.fromBase58(key); - if (!bcoin.hd.isPublic(key)) + if (!HD.isPublic(key)) throw new Error('Must add HD keys to wallet.'); if (!key.isAccount44()) @@ -278,10 +303,10 @@ Account.prototype.pushKey = function pushKey(key) { */ Account.prototype.spliceKey = function spliceKey(key) { - if (bcoin.hd.isExtended(key)) - key = bcoin.hd.fromBase58(key); + if (HD.isExtended(key)) + key = HD.fromBase58(key); - if (!bcoin.hd.isHDPublicKey(key)) + if (!HD.isHDPublicKey(key)) throw new Error('Must add HD keys to wallet.'); if (!key.isAccount44()) @@ -300,260 +325,256 @@ Account.prototype.spliceKey = function spliceKey(key) { * Add a public account key to the account (multisig). * Saves the key in the wallet database. * @param {HDPublicKey} key - * @param {Function} callback + * @returns {Promise} */ -Account.prototype.addKey = function addKey(key, callback) { - var self = this; +Account.prototype.addKey = co(function* addKey(key) { var result = false; + var exists; try { result = this.pushKey(key); } catch (e) { - return callback(e); + throw e; } - this._checkKeys(function(err, exists) { - if (err) - return callback(err); + exists = yield this._checkKeys(); - if (exists) { - self.spliceKey(key); - return callback(new Error('Cannot add a key from another account.')); - } + if (exists) { + this.spliceKey(key); + throw new Error('Cannot add a key from another account.'); + } - // Try to initialize again. - self.init(function(err) { - if (err) - return callback(err); + // Try to initialize again. + yield this.init(); - callback(null, result); - }); - }); -}; + return result; +}); /** * Ensure accounts are not sharing keys. * @private - * @param {Function} callback + * @returns {Promise} */ -Account.prototype._checkKeys = function _checkKeys(callback) { - var self = this; +Account.prototype._checkKeys = co(function* _checkKeys() { var ring, hash; if (this.initialized || this.type !== Account.types.MULTISIG) - return callback(null, false); + return false; if (this.keys.length !== this.n - 1) - return callback(null, false); + return false; ring = this.deriveReceive(0); hash = ring.getScriptHash('hex'); - this.db.getAddressPaths(hash, function(err, paths) { - if (err) - return callback(err); - - if (!paths) - return callback(null, false); - - callback(null, paths[self.wid] != null); - }); -}; + return yield this.wallet.hasAddress(hash); +}); /** * Remove a public account key from the account (multisig). * Remove the key from the wallet database. * @param {HDPublicKey} key - * @param {Function} callback + * @returns {Promise} */ -Account.prototype.removeKey = function removeKey(key, callback) { +Account.prototype.removeKey = function removeKey(key) { var result = false; try { result = this.spliceKey(key); } catch (e) { - return callback(e); + return Promise.reject(e); } this.save(); - callback(null, result); + return Promise.resolve(result); }; /** * Create a new receiving address (increments receiveDepth). - * @returns {KeyRing} + * @returns {WalletKey} */ -Account.prototype.createReceive = function createReceive(callback) { - return this.createAddress(false, callback); +Account.prototype.createReceive = function createReceive() { + return this.createKey(0); }; /** * Create a new change address (increments receiveDepth). - * @returns {KeyRing} + * @returns {WalletKey} */ -Account.prototype.createChange = function createChange(callback) { - return this.createAddress(true, callback); +Account.prototype.createChange = function createChange() { + return this.createKey(1); +}; + +/** + * Create a new change address (increments receiveDepth). + * @returns {WalletKey} + */ + +Account.prototype.createNested = function createNested() { + return this.createKey(2); }; /** * Create a new address (increments depth). * @param {Boolean} change - * @param {Function} callback - Returns [Error, {@link KeyRing}]. + * @returns {Promise} - Returns {@link WalletKey}. */ -Account.prototype.createAddress = function createAddress(change, callback) { - var self = this; +Account.prototype.createKey = co(function* createKey(branch) { var ring, lookahead; - if (typeof change === 'function') { - callback = change; - change = false; + switch (branch) { + case 0: + ring = this.deriveReceive(this.receiveDepth); + lookahead = this.deriveReceive(this.receiveDepth + this.lookahead); + yield this.saveKey(ring); + yield this.saveKey(lookahead); + this.receiveDepth++; + this.receive = ring; + break; + case 1: + ring = this.deriveChange(this.changeDepth); + lookahead = this.deriveChange(this.changeDepth + this.lookahead); + yield this.saveKey(ring); + yield this.saveKey(lookahead); + this.changeDepth++; + this.change = ring; + break; + case 2: + ring = this.deriveNested(this.nestedDepth); + lookahead = this.deriveNested(this.nestedDepth + this.lookahead); + yield this.saveKey(ring); + yield this.saveKey(lookahead); + this.nestedDepth++; + this.nested = ring; + break; + default: + throw new Error('Bad branch: ' + branch); } - if (change) { - ring = this.deriveChange(this.changeDepth); - lookahead = this.deriveChange(this.changeDepth + this.lookahead); - this.changeDepth++; - this.changeAddress = ring; - } else { - ring = this.deriveReceive(this.receiveDepth); - lookahead = this.deriveReceive(this.receiveDepth + this.lookahead); - this.receiveDepth++; - this.receiveAddress = ring; - } + this.save(); - this.saveAddress([ring, lookahead], function(err) { - if (err) - return callback(err); - - self.save(); - - callback(null, ring); - }); -}; + return ring; +}); /** * Derive a receiving address at `index`. Do not increment depth. * @param {Number} index - * @returns {KeyRing} + * @returns {WalletKey} */ Account.prototype.deriveReceive = function deriveReceive(index, master) { - return this.deriveAddress(false, index, master); + return this.deriveKey(0, index, master); }; /** * Derive a change address at `index`. Do not increment depth. * @param {Number} index - * @returns {KeyRing} + * @returns {WalletKey} */ Account.prototype.deriveChange = function deriveChange(index, master) { - return this.deriveAddress(true, index, master); + return this.deriveKey(1, index, master); +}; + +/** + * Derive a nested address at `index`. Do not increment depth. + * @param {Number} index + * @returns {WalletKey} + */ + +Account.prototype.deriveNested = function deriveNested(index, master) { + if (!this.witness) + throw new Error('Cannot derive nested on non-witness account.'); + + return this.deriveKey(2, index, master); }; /** * Derive an address from `path` object. * @param {Path} path * @param {MasterKey} master - * @returns {KeyRing} + * @returns {WalletKey} */ Account.prototype.derivePath = function derivePath(path, master) { - var ring, script, raw; + var data = path.data; + var ring; - // Imported key. - if (path.index === -1) { - assert(path.imported); - assert(this.type === Account.types.PUBKEYHASH); + switch (path.keyType) { + case Path.types.HD: + return this.deriveKey(path.branch, path.index, master); + case Path.types.KEY: + assert(this.type === Account.types.PUBKEYHASH); - raw = path.imported; + if (path.encrypted) { + data = master.decipher(data, path.hash); + if (!data) + return; + } - if (path.encrypted) - raw = master.decipher(raw, path.hash); + ring = WalletKey.fromImport(this, data); - if (!raw) + return ring; + case Path.types.ADDRESS: return; - - ring = bcoin.keyring.fromRaw(raw, this.network); - ring.path = path; - - return ring; + default: + assert(false, 'Bad key type.'); } - - // Custom redeem script. - if (path.script) - script = bcoin.script.fromRaw(path.script); - - ring = this.deriveAddress(path.change, path.index, master, script); - - return ring; }; /** * Derive an address at `index`. Do not increment depth. - * @param {Boolean} change - Whether the address on the change branch. + * @param {Number} branch - Whether the address on the change branch. * @param {Number} index - * @returns {KeyRing} + * @returns {WalletKey} */ -Account.prototype.deriveAddress = function deriveAddress(change, index, master, script) { +Account.prototype.deriveKey = function deriveKey(branch, index, master) { var keys = []; var i, key, shared, ring; - change = +change; + assert(typeof branch === 'number'); - if (master && master.key) { + if (master && master.key && !this.watchOnly) { key = master.key.deriveAccount44(this.accountIndex); - key = key.derive(change).derive(index); + key = key.derive(branch).derive(index); } else { - key = this.accountKey.derive(change).derive(index); + key = this.accountKey.derive(branch).derive(index); } - ring = bcoin.keyring.fromPublic(key.publicKey, this.network); - ring.witness = this.witness; + ring = WalletKey.fromHD(this, key, branch, index); - if (script) { - // Custom redeem script. - assert(this.type === Account.types.PUBKEYHASH); - ring.script = script; - } else { - switch (this.type) { - case Account.types.PUBKEYHASH: - break; - case Account.types.MULTISIG: - keys.push(key.publicKey); + switch (this.type) { + case Account.types.PUBKEYHASH: + break; + case Account.types.MULTISIG: + keys.push(key.publicKey); - for (i = 0; i < this.keys.length; i++) { - shared = this.keys[i]; - shared = shared.derive(change).derive(index); - keys.push(shared.publicKey); - } + for (i = 0; i < this.keys.length; i++) { + shared = this.keys[i]; + shared = shared.derive(branch).derive(index); + keys.push(shared.publicKey); + } - ring.script = bcoin.script.fromMultisig(this.m, this.n, keys); + ring.script = Script.fromMultisig(this.m, this.n, keys); - break; - } + break; } - if (key.privateKey) - ring.privateKey = key.privateKey; - - ring.path = bcoin.path.fromAccount(this, ring, change, index); - return ring; }; /** * Save the account to the database. Necessary * when address depth and keys change. - * @param {Function} callback + * @returns {Promise} */ Account.prototype.save = function save() { @@ -562,12 +583,22 @@ Account.prototype.save = function save() { /** * Save addresses to path map. - * @param {KeyRing[]} rings - * @param {Function} callback + * @param {WalletKey[]} rings + * @returns {Promise} */ -Account.prototype.saveAddress = function saveAddress(rings, callback) { - return this.db.saveAddress(this.wid, rings, callback); +Account.prototype.saveKey = function saveKey(ring) { + return this.db.saveKey(this.wallet, ring); +}; + +/** + * Save paths to path map. + * @param {Path[]} rings + * @returns {Promise} + */ + +Account.prototype.savePath = function savePath(path) { + return this.db.savePath(this.wallet, path); }; /** @@ -575,51 +606,102 @@ Account.prototype.saveAddress = function saveAddress(rings, callback) { * Allocate all addresses up to depth. Note that this also allocates * new lookahead addresses. * @param {Number} depth - * @param {Function} callback - Returns [Error, {@link KeyRing}, {@link KeyRing}]. + * @returns {Promise} - Returns {@link WalletKey}, {@link WalletKey}. */ -Account.prototype.setDepth = function setDepth(receiveDepth, changeDepth, callback) { - var self = this; - var rings = []; - var i, receive, change; +Account.prototype.setDepth = co(function* setDepth(receiveDepth, changeDepth, nestedDepth) { + var i = -1; + var receive, change, nested, lookahead; if (receiveDepth > this.receiveDepth) { for (i = this.receiveDepth; i < receiveDepth; i++) { receive = this.deriveReceive(i); - rings.push(receive); + yield this.saveKey(receive); } - for (i = receiveDepth; i < receiveDepth + this.lookahead; i++) - rings.push(this.deriveReceive(i)); + for (i = receiveDepth; i < receiveDepth + this.lookahead; i++) { + lookahead = this.deriveReceive(i); + yield this.saveKey(lookahead); + } - this.receiveAddress = receive; + this.receive = receive; this.receiveDepth = receiveDepth; } if (changeDepth > this.changeDepth) { for (i = this.changeDepth; i < changeDepth; i++) { change = this.deriveChange(i); - rings.push(change); + yield this.saveKey(change); } - for (i = changeDepth; i < changeDepth + this.lookahead; i++) - rings.push(this.deriveChange(i)); + for (i = changeDepth; i < changeDepth + this.lookahead; i++) { + lookahead = this.deriveChange(i); + yield this.saveKey(lookahead); + } - this.changeAddress = change; + this.change = change; this.changeDepth = changeDepth; } - if (rings.length === 0) - return callback(); + if (this.witness && nestedDepth > this.nestedDepth) { + for (i = this.nestedDepth; i < nestedDepth; i++) { + nested = this.deriveNested(i); + yield this.saveKey(nested); + } - this.saveAddress(rings, function(err) { - if (err) - return callback(err); + for (i = nestedDepth; i < nestedDepth + this.lookahead; i++) { + lookahead = this.deriveNested(i); + yield this.saveKey(lookahead); + } - self.save(); + this.nested = nested; + this.nestedDepth = nestedDepth; + } - callback(null, receive, change); - }); + if (i === -1) + return; + + this.save(); + + return receive || nested; +}); + +/** + * Get witness version for a path. + * @param {Path} + * @returns {Number} + */ + +Account.prototype.getWitnessVersion = function getWitnessVersion(path) { + if (!this.witness) + return -1; + + if (path.branch === 2) + return -1; + + return 0; +}; + +/** + * Get address type for a path. + * @param {Path} path + * @returns {Number} + */ + +Account.prototype.getAddressType = function getAddressType(path) { + if (path.branch === 2) + return Script.types.SCRIPTHASH; + + if (this.witness) { + if (this.type === Acount.types.MULTISIG) + return Script.types.WITNESSSCRIPTHASH; + return Script.types.WITNESSPUBKEYHASH; + } + + if (this.type === Acount.types.MULTISIG) + return Script.types.SCRIPTHASH; + + return Script.types.PUBKEYHASH; }; /** @@ -633,19 +715,21 @@ Account.prototype.inspect = function inspect() { name: this.name, network: this.network, initialized: this.initialized, + witness: this.witness, + watchOnly: this.watchOnly, type: Account.typesByVal[this.type].toLowerCase(), m: this.m, n: this.n, - address: this.initialized - ? this.receiveAddress.getAddress() - : null, - programAddress: this.initialized - ? this.receiveAddress.getProgramAddress() - : null, - witness: this.witness, accountIndex: this.accountIndex, receiveDepth: this.receiveDepth, changeDepth: this.changeDepth, + nestedDepth: this.nestedDepth, + address: this.initialized + ? this.receive.getAddress() + : null, + nestedAddress: this.initialized && this.nested + ? this.nested.getAddress() + : null, accountKey: this.accountKey.xpubkey, keys: this.keys.map(function(key) { return key.xpubkey; @@ -661,25 +745,26 @@ Account.prototype.inspect = function inspect() { Account.prototype.toJSON = function toJSON() { return { - network: this.network.type, wid: this.wid, name: this.name, initialized: this.initialized, + witness: this.witness, + watchOnly: this.watchOnly, type: Account.typesByVal[this.type].toLowerCase(), m: this.m, n: this.n, - witness: this.witness, accountIndex: this.accountIndex, receiveDepth: this.receiveDepth, changeDepth: this.changeDepth, - receiveAddress: this.receiveAddress - ? this.receiveAddress.getAddress('base58') + nestedDepth: this.nestedDepth, + receiveAddress: this.receive + ? this.receive.getAddress('base58') : null, - programAddress: this.receiveAddress - ? this.receiveAddress.getProgramAddress('base58') + nestedAddress: this.nested + ? this.nested.getAddress('base58') : null, - changeAddress: this.changeAddress - ? this.changeAddress.getAddress('base58') + changeAddress: this.change + ? this.change.getAddress('base58') : null, accountKey: this.accountKey.xpubkey, keys: this.keys.map(function(key) { @@ -697,11 +782,11 @@ Account.prototype.toJSON = function toJSON() { Account.prototype.fromJSON = function fromJSON(json) { var i, key; - assert.equal(json.network, this.network.type); assert(utils.isNumber(json.wid)); assert(utils.isName(json.id), 'Bad wallet ID.'); assert(utils.isName(json.name), 'Bad account name.'); assert(typeof json.initialized === 'boolean'); + assert(typeof json.watchOnly === 'boolean'); assert(typeof json.type === 'string'); assert(utils.isNumber(json.m)); assert(utils.isNumber(json.n)); @@ -709,24 +794,27 @@ Account.prototype.fromJSON = function fromJSON(json) { assert(utils.isNumber(json.accountIndex)); assert(utils.isNumber(json.receiveDepth)); assert(utils.isNumber(json.changeDepth)); + assert(utils.isNumber(json.nestedDepth)); assert(Array.isArray(json.keys)); this.wid = json.wid; this.name = json.name; this.initialized = json.initialized; + this.witness = json.witness; + this.watchOnly = json.watchOnly; this.type = Account.types[json.type.toUpperCase()]; this.m = json.m; this.n = json.n; - this.witness = json.witness; this.accountIndex = json.accountIndex; this.receiveDepth = json.receiveDepth; this.changeDepth = json.changeDepth; - this.accountKey = bcoin.hd.fromBase58(json.accountKey); + this.nestedDepth = json.nestedDepth; + this.accountKey = HD.fromBase58(json.accountKey); assert(this.type != null); for (i = 0; i < json.keys.length; i++) { - key = bcoin.hd.fromBase58(json.keys[i]); + key = HD.fromBase58(json.keys[i]); this.pushKey(key); } @@ -742,16 +830,16 @@ Account.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); var i, key; - p.writeU32(this.network.magic); - p.writeVarString(this.name, 'utf8'); + p.writeVarString(this.name, 'ascii'); p.writeU8(this.initialized ? 1 : 0); + p.writeU8(this.witness ? 1 : 0); p.writeU8(this.type); p.writeU8(this.m); p.writeU8(this.n); - p.writeU8(this.witness ? 1 : 0); p.writeU32(this.accountIndex); p.writeU32(this.receiveDepth); p.writeU32(this.changeDepth); + p.writeU32(this.nestedDepth); p.writeBytes(this.accountKey.toRaw()); p.writeU8(this.keys.length); @@ -777,24 +865,24 @@ Account.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); var i, count, key; - this.network = bcoin.network.fromMagic(p.readU32()); - this.name = p.readVarString('utf8'); + this.name = p.readVarString('ascii'); this.initialized = p.readU8() === 1; + this.witness = p.readU8() === 1; this.type = p.readU8(); this.m = p.readU8(); this.n = p.readU8(); - this.witness = p.readU8() === 1; this.accountIndex = p.readU32(); this.receiveDepth = p.readU32(); this.changeDepth = p.readU32(); - this.accountKey = bcoin.hd.fromRaw(p.readBytes(82)); + this.nestedDepth = p.readU32(); + this.accountKey = HD.fromRaw(p.readBytes(82)); assert(Account.typesByVal[this.type]); count = p.readU8(); for (i = 0; i < count; i++) { - key = bcoin.hd.fromRaw(p.readBytes(82)); + key = HD.fromRaw(p.readBytes(82)); this.pushKey(key); } @@ -833,7 +921,7 @@ Account.fromJSON = function fromJSON(db, json) { Account.isAccount = function isAccount(obj) { return obj && typeof obj.receiveDepth === 'number' - && obj.deriveAddress === 'function'; + && obj.deriveKey === 'function'; }; /* diff --git a/lib/wallet/browser.js b/lib/wallet/browser.js index bf16f2e0..b2d7dcd7 100644 --- a/lib/wallet/browser.js +++ b/lib/wallet/browser.js @@ -17,6 +17,12 @@ layout.walletdb = { pp: function(key) { return key.slice(1); }, + P: function(wid, hash) { + return 'p' + pad32(wid) + hash; + }, + Pp: function(key) { + return key.slice(11); + }, w: function(wid) { return 'w' + pad32(wid); }, diff --git a/lib/wallet/index.js b/lib/wallet/index.js new file mode 100644 index 00000000..483c7cf8 --- /dev/null +++ b/lib/wallet/index.js @@ -0,0 +1,9 @@ +'use strict'; + +exports.Account = require('./account'); +exports.MasterKey = require('./masterkey'); +exports.Path = require('./path'); +exports.TXDB = require('./txdb'); +exports.WalletDB = require('./walletdb'); +exports.Wallet = require('./wallet'); +exports.WalletKey = require('./walletkey'); diff --git a/lib/wallet/masterkey.js b/lib/wallet/masterkey.js new file mode 100644 index 00000000..37ffea69 --- /dev/null +++ b/lib/wallet/masterkey.js @@ -0,0 +1,587 @@ +/*! + * masterkey.js - master bip32 key object for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var utils = require('../utils/utils'); +var Locker = require('../utils/locker'); +var co = require('../utils/co'); +var crypto = require('../crypto/crypto'); +var assert = require('assert'); +var BufferReader = require('../utils/reader'); +var BufferWriter = require('../utils/writer'); +var HD = require('../hd/hd'); + +/** + * Master BIP32 key which can exist + * in a timed out encrypted state. + * @exports Master + * @constructor + * @param {Object} options + */ + +function MasterKey(options) { + if (!(this instanceof MasterKey)) + return new MasterKey(options); + + this.encrypted = false; + this.iv = null; + this.ciphertext = null; + this.key = null; + + this.alg = MasterKey.alg.PBKDF2; + this.N = 50000; + this.r = 0; + this.p = 0; + + this.aesKey = null; + this.timer = null; + this.until = 0; + this._destroy = this.destroy.bind(this); + this.locker = new Locker(this); + + if (options) + this.fromOptions(options); +} + +/** + * Key derivation algorithms. + * @enum {Number} + * @default + */ + +MasterKey.alg = { + PBKDF2: 0, + SCRYPT: 1 +}; + +/** + * Key derivation algorithms by value. + * @enum {String} + * @default + */ + +MasterKey.algByVal = { + 0: 'pbkdf2', + 1: 'scrypt' +}; + +/** + * Inject properties from options object. + * @private + * @param {Object} options + */ + +MasterKey.prototype.fromOptions = function fromOptions(options) { + assert(options); + + if (options.encrypted != null) { + assert(typeof options.encrypted === 'boolean'); + this.encrypted = options.encrypted; + } + + if (options.iv) { + assert(Buffer.isBuffer(options.iv)); + this.iv = options.iv; + } + + if (options.ciphertext) { + assert(Buffer.isBuffer(options.ciphertext)); + this.ciphertext = options.ciphertext; + } + + if (options.key) { + assert(HD.isHD(options.key)); + this.key = options.key; + } + + if (options.alg != null) { + if (typeof options.alg === 'string') { + this.alg = MasterKey.alg[options.alg.toLowerCase()]; + assert(this.alg != null, 'Unknown algorithm.'); + } else { + assert(typeof options.alg === 'number'); + assert(MasterKey.algByVal[options.alg]); + this.alg = options.alg; + } + } + + if (options.rounds != null) { + assert(utils.isNumber(options.rounds)); + this.N = options.rounds; + } + + if (options.N != null) { + assert(utils.isNumber(options.N)); + this.N = options.N; + } + + if (options.r != null) { + assert(utils.isNumber(options.r)); + this.r = options.r; + } + + if (options.p != null) { + assert(utils.isNumber(options.p)); + this.p = options.p; + } + + assert(this.encrypted ? !this.key : this.key); + + return this; +}; + +/** + * Instantiate master key from options. + * @returns {MasterKey} + */ + +MasterKey.fromOptions = function fromOptions(options) { + return new MasterKey().fromOptions(options); +}; + +/** + * Decrypt the key and set a timeout to destroy decrypted data. + * @param {Buffer|String} passphrase - Zero this yourself. + * @param {Number} [timeout=60000] timeout in ms. + * @returns {Promise} - Returns {@link HDPrivateKey}. + */ + +MasterKey.prototype.unlock = co(function* _unlock(passphrase, timeout) { + var unlock = yield this.locker.lock(); + try { + return yield this._unlock(passphrase, timeout); + } finally { + unlock(); + } +}); + +/** + * Decrypt the key without a lock. + * @private + * @param {Buffer|String} passphrase - Zero this yourself. + * @param {Number} [timeout=60000] timeout in ms. + * @returns {Promise} - Returns {@link HDPrivateKey}. + */ + +MasterKey.prototype._unlock = co(function* _unlock(passphrase, timeout) { + var data, key; + + if (this.key) + return this.key; + + if (!passphrase) + throw new Error('No passphrase.'); + + assert(this.encrypted); + + key = yield this.derive(passphrase); + data = crypto.decipher(this.ciphertext, key, this.iv); + + this.key = HD.fromExtended(data); + + this.start(timeout); + + this.aesKey = key; + + return this.key; +}); + +/** + * Start the destroy timer. + * @private + * @param {Number} [timeout=60000] timeout in ms. + */ + +MasterKey.prototype.start = function start(timeout) { + if (!timeout) + timeout = 60000; + + this.stop(); + + if (timeout === -1) + return; + + this.until = utils.now() + (timeout / 1000 | 0); + this.timer = setTimeout(this._destroy, timeout); +}; + +/** + * Stop the destroy timer. + * @private + */ + +MasterKey.prototype.stop = function stop() { + if (this.timer != null) { + clearTimeout(this.timer); + this.timer = null; + this.until = 0; + } +}; + +/** + * Derive an aes key based on params. + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + +MasterKey.prototype.derive = function derive(passwd) { + switch (this.alg) { + case MasterKey.alg.PBKDF2: + return crypto.pbkdf2Async(passwd, 'bcoin', this.N, 32, 'sha256'); + case MasterKey.alg.SCRYPT: + return crypto.scryptAsync(passwd, 'bcoin', this.N, this.r, this.p, 32); + default: + return Promise.reject(new Error('Unknown algorithm: ' + this.alg)); + } +}; + +/** + * Encrypt data with in-memory aes key. + * @param {Buffer} data + * @param {Buffer} iv + * @returns {Buffer} + */ + +MasterKey.prototype.encipher = function encipher(data, iv) { + if (!this.aesKey) + return; + + if (typeof iv === 'string') + iv = new Buffer(iv, 'hex'); + + return crypto.encipher(data, this.aesKey, iv.slice(0, 16)); +}; + +/** + * Decrypt data with in-memory aes key. + * @param {Buffer} data + * @param {Buffer} iv + * @returns {Buffer} + */ + +MasterKey.prototype.decipher = function decipher(data, iv) { + if (!this.aesKey) + return; + + if (typeof iv === 'string') + iv = new Buffer(iv, 'hex'); + + return crypto.decipher(data, this.aesKey, iv.slice(0, 16)); +}; + +/** + * Destroy the key by zeroing the + * privateKey and chainCode. Stop + * the timer if there is one. + */ + +MasterKey.prototype.destroy = function destroy() { + if (!this.encrypted) { + assert(this.timer == null); + assert(this.key); + return; + } + + this.stop(); + + if (this.key) { + this.key.destroy(true); + this.key = null; + } + + if (this.aesKey) { + this.aesKey.fill(0); + this.aesKey = null; + } +}; + +/** + * Decrypt the key permanently. + * @param {Buffer|String} passphrase - Zero this yourself. + * @returns {Promise} + */ + +MasterKey.prototype.decrypt = co(function* decrypt(passphrase) { + var unlock = yield this.locker.lock(); + try { + return yield this._decrypt(passphrase); + } finally { + unlock(); + } +}); + +/** + * Decrypt the key permanently without a lock. + * @private + * @param {Buffer|String} passphrase - Zero this yourself. + * @returns {Promise} + */ + +MasterKey.prototype._decrypt = co(function* decrypt(passphrase) { + var key, data; + + if (!this.encrypted) { + assert(this.key); + return; + } + + if (!passphrase) + return; + + this.destroy(); + + key = yield this.derive(passphrase); + data = crypto.decipher(this.ciphertext, key, this.iv); + + this.key = HD.fromExtended(data); + this.encrypted = false; + this.iv = null; + this.ciphertext = null; + + return key; +}); + +/** + * Encrypt the key permanently. + * @param {Buffer|String} passphrase - Zero this yourself. + * @returns {Promise} + */ + +MasterKey.prototype.encrypt = co(function* encrypt(passphrase) { + var unlock = yield this.locker.lock(); + try { + return yield this._encrypt(passphrase); + } finally { + unlock(); + } +}); + +/** + * Encrypt the key permanently without a lock. + * @private + * @param {Buffer|String} passphrase - Zero this yourself. + * @returns {Promise} + */ + +MasterKey.prototype._encrypt = co(function* encrypt(passphrase) { + var key, data, iv; + + if (this.encrypted) + return; + + if (!passphrase) + return; + + data = this.key.toExtended(); + iv = crypto.randomBytes(16); + + this.stop(); + + key = yield this.derive(passphrase); + data = crypto.encipher(data, key, iv); + + this.key = null; + this.encrypted = true; + this.iv = iv; + this.ciphertext = data; + + return key; +}); + +/** + * Serialize the key in the form of: + * `[enc-flag][iv?][ciphertext?][extended-key?]` + * @returns {Buffer} + */ + +MasterKey.prototype.toRaw = function toRaw(writer) { + var p = new BufferWriter(writer); + + if (this.encrypted) { + p.writeU8(1); + p.writeVarBytes(this.iv); + p.writeVarBytes(this.ciphertext); + + p.writeU8(this.alg); + p.writeU32(this.N); + p.writeU32(this.r); + p.writeU32(this.p); + + if (!writer) + p = p.render(); + + return p; + } + + p.writeU8(0); + p.writeVarBytes(this.key.toExtended()); + + if (!writer) + p = p.render(); + + return p; +}; + +/** + * Inject properties from serialized data. + * @private + * @param {Buffer} raw + */ + +MasterKey.prototype.fromRaw = function fromRaw(raw) { + var p = new BufferReader(raw); + + this.encrypted = p.readU8() === 1; + + if (this.encrypted) { + this.iv = p.readVarBytes(); + this.ciphertext = p.readVarBytes(); + + this.alg = p.readU8(); + + assert(MasterKey.algByVal[this.alg]); + + this.N = p.readU32(); + this.r = p.readU32(); + this.p = p.readU32(); + + return this; + } + + this.key = HD.fromExtended(p.readVarBytes()); + + return this; +}; + +/** + * Instantiate master key from serialized data. + * @returns {MasterKey} + */ + +MasterKey.fromRaw = function fromRaw(raw) { + return new MasterKey().fromRaw(raw); +}; + +/** + * Inject properties from an HDPrivateKey. + * @private + * @param {HDPrivateKey} key + */ + +MasterKey.prototype.fromKey = function fromKey(key) { + this.encrypted = false; + this.iv = null; + this.ciphertext = null; + this.key = key; + return this; +}; + +/** + * Instantiate master key from an HDPrivateKey. + * @param {HDPrivateKey} key + * @returns {MasterKey} + */ + +MasterKey.fromKey = function fromKey(key) { + return new MasterKey().fromKey(key); +}; + +/** + * Convert master key to a jsonifiable object. + * @returns {Object} + */ + +MasterKey.prototype.toJSON = function toJSON() { + if (this.encrypted) { + return { + encrypted: true, + iv: this.iv.toString('hex'), + ciphertext: this.ciphertext.toString('hex'), + algorithm: MasterKey.algByVal[this.alg], + N: this.N, + r: this.r, + p: this.p + }; + } + + return { + encrypted: false, + key: this.key.toJSON() + }; +}; + +/** + * Inject properties from JSON object. + * @private + * @param {Object} json + */ + +MasterKey.prototype.fromJSON = function fromJSON(json) { + assert(typeof json.encrypted === 'boolean'); + + this.encrypted = json.encrypted; + + if (json.encrypted) { + assert(typeof json.iv === 'string'); + assert(typeof json.ciphertext === 'string'); + assert(typeof json.algorithm === 'string'); + assert(utils.isNumber(json.N)); + assert(utils.isNumber(json.r)); + assert(utils.isNumber(json.p)); + this.iv = new Buffer(json.iv, 'hex'); + this.ciphertext = new Buffer(json.ciphertext, 'hex'); + this.alg = MasterKey.alg[json.algorithm]; + assert(this.alg != null); + this.N = json.N; + this.r = json.r; + this.p = json.p; + } else { + this.key = HD.fromJSON(json.key); + } + + return this; +}; + +/** + * Instantiate master key from jsonified object. + * @param {Object} json + * @returns {MasterKey} + */ + +MasterKey.fromJSON = function fromJSON(json) { + return new MasterKey().fromJSON(json); +}; + +/** + * Inspect the key. + * @returns {Object} + */ + +MasterKey.prototype.inspect = function inspect() { + var json = this.toJSON(); + if (this.key) + json.key = this.key.toJSON(); + return json; +}; + +/** + * Test whether an object is a MasterKey. + * @param {Object} obj + * @returns {Boolean} + */ + +MasterKey.isMasterKey = function isMasterKey(obj) { + return obj + && typeof obj.encrypted === 'boolean' + && typeof obj.decrypt === 'function'; +}; + +/* + * Expose + */ + +module.exports = MasterKey; diff --git a/lib/wallet/path.js b/lib/wallet/path.js index 1228b027..844c6b2b 100644 --- a/lib/wallet/path.js +++ b/lib/wallet/path.js @@ -6,12 +6,11 @@ 'use strict'; -var bcoin = require('../env'); -var utils = require('../utils/utils'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); +var Address = require('../primitives/address'); +var Script = require('../script/script'); /** * Path @@ -20,34 +19,85 @@ var BufferWriter = require('../utils/writer'); * @property {WalletID} wid * @property {String} name - Account name. * @property {Number} account - Account index. - * @property {Number} change - Change index. + * @property {Number} branch - Branch index. * @property {Number} index - Address index. * @property {Address|null} address */ -function Path() { +function Path(options) { if (!(this instanceof Path)) - return new Path(); + return new Path(options); - this.wid = null; - this.name = null; + this.keyType = Path.types.HD; + + this.id = null; // Passed in by caller. + this.wid = -1; // Passed in by caller. + this.name = null; // Passed in by caller. this.account = 0; - this.change = -1; + this.branch = -1; this.index = -1; this.encrypted = false; - this.imported = null; - this.script = null; + this.data = null; // Currently unused. - this.type = bcoin.script.types.PUBKEYHASH; + this.type = Script.types.PUBKEYHASH; this.version = -1; + this.hash = null; // Passed in by caller. - // Passed in by caller. - this.id = null; - this.hash = null; + if (options) + this.fromOptions(options); } +/** + * Path types. + * @enum {Number} + * @default + */ + +Path.types = { + HD: 0, + KEY: 1, + ADDRESS: 2 +}; + +/** + * Instantiate path from options object. + * @private + * @param {Object} options + * @returns {Path} + */ + +Path.prototype.fromOptions = function fromOptions(options) { + this.keyType = options.keyType; + + this.id = options.id; + this.wid = options.wid; + this.name = options.name; + this.account = options.account; + this.branch = options.branch; + this.index = options.index; + + this.encrypted = options.encrypted; + this.data = options.data; + + this.type = options.type; + this.version = options.version; + this.hash = options.hash; + + return this; +}; + +/** + * Instantiate path from options object. + * @param {Object} options + * @returns {Path} + */ + +Path.fromOptions = function fromOptions(options) { + return new Path().fromOptions(options); +}; + /** * Clone the path object. * @returns {Path} @@ -56,20 +106,20 @@ function Path() { Path.prototype.clone = function clone() { var path = new Path(); + path.keyType = this.keyType; + + path.id = this.id; path.wid = this.wid; path.name = this.name; path.account = this.account; - path.change = this.change; + path.branch = this.branch; path.index = this.index; path.encrypted = this.encrypted; - path.imported = this.imported; - path.script = this.script; + path.data = this.data; path.type = this.type; path.version = this.version; - - path.id = this.id; path.hash = this.hash; return path; @@ -84,22 +134,20 @@ Path.prototype.clone = function clone() { Path.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); - this.wid = p.readU32(); - this.name = p.readVarString('utf8'); this.account = p.readU32(); + this.keyType = p.readU8(); - switch (p.readU8()) { - case 0: - this.change = p.readU32(); + switch (this.keyType) { + case Path.types.HD: + this.branch = p.readU32(); this.index = p.readU32(); - if (p.readU8() === 1) - this.script = p.readVarBytes(); break; - case 1: + case Path.types.KEY: this.encrypted = p.readU8() === 1; - this.imported = p.readVarBytes(); - this.change = -1; - this.index = -1; + this.data = p.readVarBytes(); + break; + case Path.types.ADDRESS: + // Hash will be passed in by caller. break; default: assert(false); @@ -130,26 +178,29 @@ Path.fromRaw = function fromRaw(data) { Path.prototype.toRaw = function toRaw(writer) { var p = new BufferWriter(writer); - p.writeU32(this.wid); - p.writeVarString(this.name, 'utf8'); p.writeU32(this.account); + p.writeU8(this.keyType); - if (this.index !== -1) { - assert(!this.imported); - p.writeU8(0); - p.writeU32(this.change); - p.writeU32(this.index); - if (this.script) { - p.writeU8(1); - p.writeVarBytes(this.script); - } else { - p.writeU8(0); - } - } else { - assert(this.imported); - p.writeU8(1); - p.writeU8(this.encrypted ? 1 : 0); - p.writeVarBytes(this.imported); + switch (this.keyType) { + case Path.types.HD: + assert(!this.data); + assert(this.index !== -1); + p.writeU32(this.branch); + p.writeU32(this.index); + break; + case Path.types.KEY: + assert(this.data); + assert(this.index === -1); + p.writeU8(this.encrypted ? 1 : 0); + p.writeVarBytes(this.data); + break; + case Path.types.ADDRESS: + assert(!this.data); + assert(this.index === -1); + break; + default: + assert(false); + break; } p.write8(this.version); @@ -162,41 +213,33 @@ Path.prototype.toRaw = function toRaw(writer) { }; /** - * Inject properties from account. + * Inject properties from address. * @private - * @param {WalletID} wid - * @param {KeyRing} ring + * @param {Account} account + * @param {Address} address */ -Path.prototype.fromAccount = function fromAccount(account, ring, change, index) { +Path.prototype.fromAddress = function fromAddress(account, address) { + this.keyType = Path.types.ADDRESS; + this.id = account.id; this.wid = account.wid; this.name = account.name; this.account = account.accountIndex; - - if (change != null) - this.change = change; - - if (index != null) - this.index = index; - - this.version = ring.witness ? 0 : -1; - this.type = ring.getType(); - - this.id = account.id; - this.hash = ring.getHash('hex'); - + this.version = address.version; + this.type = address.type; + this.hash = address.getHash('hex'); return this; }; /** - * Instantiate path from keyring. - * @param {WalletID} wid - * @param {KeyRing} ring + * Instantiate path from address. + * @param {Account} account + * @param {Address} address * @returns {Path} */ -Path.fromAccount = function fromAccount(account, ring, change, index) { - return new Path().fromAccount(account, ring, change, index); +Path.fromAddress = function fromAddress(account, address) { + return new Path().fromAddress(account, address); }; /** @@ -205,8 +248,11 @@ Path.fromAccount = function fromAccount(account, ring, change, index) { */ Path.prototype.toPath = function toPath() { + if (this.keyType !== Path.types.HD) + return null; + return 'm/' + this.account - + '\'/' + this.change + + '\'/' + this.branch + '/' + this.index; }; @@ -216,7 +262,7 @@ Path.prototype.toPath = function toPath() { */ Path.prototype.toAddress = function toAddress(network) { - return bcoin.address.fromHash(this.hash, this.type, this.version, network); + return Address.fromHash(this.hash, this.type, this.version, network); }; /** @@ -227,44 +273,12 @@ Path.prototype.toAddress = function toAddress(network) { Path.prototype.toJSON = function toJSON() { return { name: this.name, - change: this.change === 1, - path: this.toPath() + account: this.account, + change: this.branch === 1, + derivation: this.toPath() }; }; -/** - * Inject properties from json object. - * @private - * @param {Object} json - */ - -Path.prototype.fromJSON = function fromJSON(json) { - var indexes = bcoin.hd.parsePath(json.path, constants.hd.MAX_INDEX); - - assert(indexes.length === 3); - assert(indexes[0] >= constants.hd.HARDENED); - indexes[0] -= constants.hd.HARDENED; - - this.wid = json.wid; - this.id = json.id; - this.name = json.name; - this.account = indexes[0]; - this.change = indexes[1]; - this.index = indexes[2]; - - return this; -}; - -/** - * Instantiate path from json object. - * @param {Object} json - * @returns {Path} - */ - -Path.fromJSON = function fromJSON(json) { - return new Path().fromJSON(json); -}; - /** * Inspect the path. * @returns {String} diff --git a/lib/wallet/pathinfo.js b/lib/wallet/pathinfo.js new file mode 100644 index 00000000..444c5dde --- /dev/null +++ b/lib/wallet/pathinfo.js @@ -0,0 +1,309 @@ +/*! + * pathinfo.js - pathinfo object for bcoin + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var utils = require('../utils/utils'); + +/** + * Path Info + * @constructor + * @param {WalletDB} db + * @param {WalletID} wid + * @param {TX} tx + * @param {Object} table + */ + +function PathInfo(wallet, tx, paths) { + if (!(this instanceof PathInfo)) + return new PathInfo(wallet, tx, paths); + + // All relevant Accounts for + // inputs and outputs (for database indexing). + this.accounts = []; + + // All output paths (for deriving during sync). + this.paths = []; + + // Wallet + this.wallet = wallet; + + // Wallet ID + this.wid = wallet.wid; + + // Wallet Label + this.id = wallet.id; + + // Map of address hashes->paths. + this.pathMap = {}; + + // Current transaction. + this.tx = null; + + // Wallet-specific details cache. + this._details = null; + this._json = null; + + if (tx) + this.fromTX(tx, paths); +} + +/** + * Instantiate path info from a transaction. + * @private + * @param {TX} tx + * @param {Object} table + * @returns {PathInfo} + */ + +PathInfo.prototype.fromTX = function fromTX(tx, paths) { + var uniq = {}; + var i, hashes, hash, path; + + this.tx = tx; + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + + this.pathMap[path.hash] = path; + + if (!uniq[path.account]) { + uniq[path.account] = true; + this.accounts.push(path.account); + } + } + + hashes = tx.getOutputHashes('hex'); + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + path = this.pathMap[hash]; + if (path) + this.paths.push(path); + } + + return this; +}; + +/** + * Instantiate path info from a transaction. + * @param {WalletDB} db + * @param {WalletID} wid + * @param {TX} tx + * @param {Object} table + * @returns {PathInfo} + */ + +PathInfo.fromTX = function fromTX(wallet, tx, paths) { + return new PathInfo(wallet).fromTX(tx, paths); +}; + +/** + * Test whether the map has paths + * for a given address hash. + * @param {Hash} hash + * @returns {Boolean} + */ + +PathInfo.prototype.hasPath = function hasPath(hash) { + if (!hash) + return false; + + return this.pathMap[hash] != null; +}; + +/** + * Get path for a given address hash. + * @param {Hash} hash + * @returns {Path} + */ + +PathInfo.prototype.getPath = function getPath(hash) { + if (!hash) + return; + + return this.pathMap[hash]; +}; + +/** + * Convert path info to transaction details. + * @returns {Details} + */ + +PathInfo.prototype.toDetails = function toDetails() { + var details = this._details; + + if (!details) { + details = new Details(this); + this._details = details; + } + + return details; +}; + +/** + * Convert path info to JSON details (caches json). + * @returns {Object} + */ + +PathInfo.prototype.toJSON = function toJSON() { + var json = this._json; + + if (!json) { + json = this.toDetails().toJSON(); + this._json = json; + } + + return json; +}; + +/** + * Transaction Details + * @constructor + * @param {PathInfo} info + */ + +function Details(info) { + if (!(this instanceof Details)) + return new Details(info); + + this.db = info.wallet.db; + this.network = this.db.network; + this.wid = info.wid; + this.id = info.id; + this.hash = info.tx.hash('hex'); + this.height = info.tx.height; + this.block = info.tx.block; + this.index = info.tx.index; + this.confirmations = info.tx.getConfirmations(this.db.height); + this.fee = info.tx.getFee(); + this.ts = info.tx.ts; + this.ps = info.tx.ps; + this.tx = info.tx; + this.inputs = []; + this.outputs = []; + + this.init(info.pathMap); +} + +/** + * Initialize transactions details + * by pushing on mapped members. + * @private + * @param {Object} table + */ + +Details.prototype.init = function init(map) { + this._insert(this.tx.inputs, true, this.inputs, map); + this._insert(this.tx.outputs, false, this.outputs, map); +}; + +/** + * Insert members in the input or output vector. + * @private + * @param {Input[]|Output[]} vector + * @param {Array} target + * @param {Object} table + */ + +Details.prototype._insert = function _insert(vector, input, target, map) { + var i, io, address, hash, path, member; + + for (i = 0; i < vector.length; i++) { + io = vector[i]; + member = new DetailsMember(); + + if (input) { + if (io.coin) + member.value = io.coin.value; + } else { + member.value = io.value; + } + + address = io.getAddress(); + + if (address) { + member.address = address; + + hash = address.getHash('hex'); + path = map[hash]; + + if (path) + member.path = path; + } + + target.push(member); + } +}; + +/** + * Convert details to a more json-friendly object. + * @returns {Object} + */ + +Details.prototype.toJSON = function toJSON() { + var self = this; + return { + wid: this.wid, + id: this.id, + hash: utils.revHex(this.hash), + height: this.height, + block: this.block ? utils.revHex(this.block) : null, + ts: this.ts, + ps: this.ps, + index: this.index, + fee: utils.btc(this.fee), + confirmations: this.confirmations, + inputs: this.inputs.map(function(input) { + return input.toJSON(self.network); + }), + outputs: this.outputs.map(function(output) { + return output.toJSON(self.network); + }), + tx: this.tx.toRaw().toString('hex') + }; +}; + +/** + * Transaction Details Member + * @constructor + * @property {Number} value + * @property {Address} address + * @property {Path} path + */ + +function DetailsMember() { + if (!(this instanceof DetailsMember)) + return new DetailsMember(); + + this.value = 0; + this.address = null; + this.path = null; +} + +/** + * Convert the member to a more json-friendly object. + * @param {Network} network + * @returns {Object} + */ + +DetailsMember.prototype.toJSON = function toJSON(network) { + return { + value: utils.btc(this.value), + address: this.address + ? this.address.toBase58(network) + : null, + path: this.path + ? this.path.toJSON() + : null + }; +}; + +/* + * Expose + */ + +module.exports = PathInfo; diff --git a/lib/wallet/txdb.js b/lib/wallet/txdb.js index 67ff20b8..8644db04 100644 --- a/lib/wallet/txdb.js +++ b/lib/wallet/txdb.js @@ -7,13 +7,17 @@ 'use strict'; -var bcoin = require('../env'); var utils = require('../utils/utils'); -var assert = bcoin.utils.assert; -var constants = bcoin.constants; -var DUMMY = new Buffer([0]); +var LRU = require('../utils/lru'); +var co = require('../utils/co'); +var assert = require('assert'); +var constants = require('../protocol/constants'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); +var TX = require('../primitives/tx'); +var Coin = require('../primitives/coin'); +var Outpoint = require('../primitives/outpoint'); +var DUMMY = new Buffer([0]); /* * Database Layout: @@ -209,10 +213,7 @@ function TXDB(wallet) { this.options = wallet.db.options; this.locked = {}; - this.locker = new bcoin.locker(this); - this.coinCache = new bcoin.lru(10000); - - this.current = null; + this.coinCache = new LRU(10000); this.balance = null; } @@ -225,28 +226,18 @@ TXDB.layout = layout; /** * Open TXDB. - * @param {Function} callback + * @returns {Promise} */ -TXDB.prototype.open = function open(callback) { - var self = this; - - this.getBalance(function(err, balance) { - if (err) - return callback(err); - - self.logger.info('TXDB loaded for %s.', self.wallet.id); - self.logger.info( - 'Balance: unconfirmed=%s confirmed=%s total=%s.', - utils.btc(balance.unconfirmed), - utils.btc(balance.confirmed), - utils.btc(balance.total)); - - self.balance = balance; - - callback(); - }); -}; +TXDB.prototype.open = co(function* open() { + this.balance = yield this.getBalance(); + this.logger.info('TXDB loaded for %s.', this.wallet.id); + this.logger.info( + 'Balance: unconfirmed=%s confirmed=%s total=%s.', + utils.btc(this.balance.unconfirmed), + utils.btc(this.balance.confirmed), + utils.btc(this.balance.total)); +}); /** * Emit transaction event. @@ -261,16 +252,6 @@ TXDB.prototype.emit = function emit(event, tx, info) { this.wallet.emit(event, tx, info); }; -/** - * Invoke the mutex lock. - * @private - * @returns {Function} unlock - */ - -TXDB.prototype._lock = function _lock(func, args, force) { - return this.locker.lock(func, args, force); -}; - /** * Prefix a key. * @param {Buffer} key @@ -282,17 +263,6 @@ TXDB.prototype.prefix = function prefix(key) { return layout.prefix(this.wallet.wid, key); }; -/** - * Start a batch. - * @returns {Batch} - */ - -TXDB.prototype.start = function start() { - assert(!this.current); - this.current = this.db.batch(); - return this.current; -}; - /** * Put key and value to current batch. * @param {String} key @@ -300,8 +270,8 @@ TXDB.prototype.start = function start() { */ TXDB.prototype.put = function put(key, value) { - assert(this.current); - this.current.put(this.prefix(key), value); + assert(this.wallet.current); + this.wallet.current.put(this.prefix(key), value); }; /** @@ -310,38 +280,8 @@ TXDB.prototype.put = function put(key, value) { */ TXDB.prototype.del = function del(key) { - assert(this.current); - this.current.del(this.prefix(key)); -}; - -/** - * Get current batch. - * @returns {Batch} - */ - -TXDB.prototype.batch = function batch() { - assert(this.current); - return this.current; -}; - -/** - * Drop current batch. - * @returns {Batch} - */ - -TXDB.prototype.drop = function drop() { - assert(this.current); - this.current.clear(); - this.current = null; -}; - -/** - * Fetch. - * @param {String} key - */ - -TXDB.prototype.fetch = function fetch(key, parse, callback) { - this.db.fetch(this.prefix(key), parse, callback); + assert(this.wallet.current); + this.wallet.current.del(this.prefix(key)); }; /** @@ -349,8 +289,8 @@ TXDB.prototype.fetch = function fetch(key, parse, callback) { * @param {String} key */ -TXDB.prototype.get = function get(key, callback) { - this.db.get(this.prefix(key), callback); +TXDB.prototype.get = function get(key) { + return this.db.get(this.prefix(key)); }; /** @@ -358,50 +298,60 @@ TXDB.prototype.get = function get(key, callback) { * @param {String} key */ -TXDB.prototype.has = function has(key, callback) { - this.db.has(this.prefix(key), callback); +TXDB.prototype.has = function has(key) { + return this.db.has(this.prefix(key)); }; /** * Iterate. * @param {Object} options - * @param {Function} callback + * @returns {Promise} */ -TXDB.prototype.iterate = function iterate(options, callback) { +TXDB.prototype.range = function range(options) { if (options.gte) options.gte = this.prefix(options.gte); if (options.lte) options.lte = this.prefix(options.lte); - this.db.iterate(options, callback); + return this.db.range(options); }; /** - * Commit current batch. - * @param {Function} callback + * Iterate. + * @param {Object} options + * @returns {Promise} */ -TXDB.prototype.commit = function commit(callback) { - var self = this; - assert(this.current); - this.current.write(function(err) { - if (err) { - self.current = null; - return callback(err); - } - self.current = null; - callback(); - }); +TXDB.prototype.keys = function keys(options) { + if (options.gte) + options.gte = this.prefix(options.gte); + if (options.lte) + options.lte = this.prefix(options.lte); + return this.db.keys(options); +}; + +/** + * Iterate. + * @param {Object} options + * @returns {Promise} + */ + +TXDB.prototype.values = function values(options) { + if (options.gte) + options.gte = this.prefix(options.gte); + if (options.lte) + options.lte = this.prefix(options.lte); + return this.db.values(options); }; /** * Map a transactions' addresses to wallet IDs. * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link PathInfo}]. + * @returns {Promise} - Returns {@link PathInfo}. */ -TXDB.prototype.getInfo = function getInfo(tx, callback) { - this.walletdb.getPathInfo(this.wallet, tx, callback); +TXDB.prototype.getPathInfo = function getPathInfo(tx) { + return this.wallet.getPathInfo(tx); }; /** @@ -409,74 +359,54 @@ TXDB.prototype.getInfo = function getInfo(tx, callback) { * to orphan list. Stored by its required coin ID. * @private * @param {Outpoint} prevout - Required coin hash & index. - * @param {Buffer} spender - Spender input hash and index. - * @param {Function} callback - Returns [Error, Buffer]. + * @param {Buffer} input - Spender input hash and index. + * @returns {Promise} - Returns Buffer. */ -TXDB.prototype._addOrphan = function _addOrphan(prevout, spender, callback) { - var self = this; - var p = new BufferWriter(); +TXDB.prototype.addOrphan = co(function* addOrphan(prevout, input) { var key = layout.o(prevout.hash, prevout.index); + var data = yield this.get(key); + var p = new BufferWriter(); - this.get(key, function(err, data) { - if (err) - return callback(err); + if (data) + p.writeBytes(data); - if (data) - p.writeBytes(data); + p.writeBytes(input); - p.writeBytes(spender); - - self.put(key, p.render()); - - callback(); - }); -}; + this.put(key, p.render()); +}); /** * Retrieve orphan list by coin ID. * @private * @param {Hash} hash * @param {Number} index - * @param {Function} callback - Returns [Error, {@link Orphan}]. + * @returns {Promise} - Returns {@link Orphan}. */ -TXDB.prototype._getOrphans = function _getOrphans(hash, index, callback) { - var self = this; +TXDB.prototype.getOrphans = co(function* getOrphans(hash, index) { + var key = layout.o(hash, index); + var data = yield this.get(key); var items = []; + var i, inputs, input, tx, p; - this.fetch(layout.o(hash, index), function(data) { - var p = new BufferReader(data); - var orphans = []; + if (!data) + return; - while (p.left()) - orphans.push(bcoin.outpoint.fromRaw(p)); + p = new BufferReader(data); + inputs = []; - return orphans; - }, function(err, orphans) { - if (err) - return callback(err); + while (p.left()) + inputs.push(Outpoint.fromRaw(p)); - if (!orphans) - return callback(); + for (i = 0; i < inputs.length; i++) { + input = inputs[i]; + tx = yield this.getTX(input.hash); + items.push(new Orphan(input, tx)); + } - utils.forEachSerial(orphans, function(orphan, next) { - self.getTX(orphan.hash, function(err, tx) { - if (err) - return next(err); - - items.push([orphan, tx]); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, items); - }); - }); -}; + return items; +}); /** * Retrieve coins for own inputs, remove @@ -484,305 +414,286 @@ TXDB.prototype._getOrphans = function _getOrphans(hash, index, callback) { * @private * @param {TX} tx * @param {PathInfo} info - * @param {Function} callback - Returns [Error]. + * @returns {Promise} */ -TXDB.prototype._verify = function _verify(tx, info, callback) { - var self = this; +TXDB.prototype.verify = co(function* verify(tx, info) { + var i, input, prevout, address, coin, spent, conflict; - utils.forEachSerial(tx.inputs, function(input, next, i) { - var prevout = input.prevout; - var address; + if (tx.isCoinbase()) + return true; - if (tx.isCoinbase()) - return next(); + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + prevout = input.prevout; address = input.getHash('hex'); // Only bother if this input is ours. if (!info.hasPath(address)) - return next(); + continue; - self.getCoin(prevout.hash, prevout.index, function(err, coin) { - if (err) - return next(err); + coin = yield this.getCoin(prevout.hash, prevout.index); - if (coin) { - // Add TX to inputs and spend money - input.coin = coin; + if (coin) { + // Add TX to inputs and spend money + input.coin = coin; - // Skip invalid transactions - if (self.options.verify) { - if (!tx.verifyInput(i)) - return callback(null, false); - } - - return next(); + // Skip invalid transactions + if (this.options.verify) { + if (!(yield tx.verifyInputAsync(i))) + return false; } - input.coin = null; + continue; + } - self.isSpent(prevout.hash, prevout.index, function(err, spent) { - if (err) - return next(err); + input.coin = null; - // Are we double-spending? - // Replace older txs with newer ones. - if (!spent) - return next(); + spent = yield this.isSpent(prevout.hash, prevout.index); - self.getSpentCoin(spent, prevout, function(err, coin) { - if (err) - return next(err); + // Are we double-spending? + // Replace older txs with newer ones. + if (!spent) + continue; - if (!coin) - return callback(new Error('Could not find double-spent coin.')); + coin = yield this.getSpentCoin(spent, prevout); - input.coin = coin; + if (!coin) + throw new Error('Could not find double-spent coin.'); - // Skip invalid transactions - if (self.options.verify) { - if (!tx.verifyInput(i)) - return callback(null, false); - } + input.coin = coin; - self.logger.warning('Removing conflicting tx: %s.', - utils.revHex(spent.hash)); + // Skip invalid transactions + if (this.options.verify) { + if (!(yield tx.verifyInputAsync(i))) + return false; + } - self._removeConflict(spent.hash, tx, function(err, tx, info) { - if (err) - return next(err); + this.logger.warning('Handling conflicting tx: %s.', + utils.revHex(spent.hash)); - // Spender was not removed, the current - // transaction is not elligible to be added. - if (!tx) - return callback(null, false); + // Remove the older double spender. + conflict = yield this.removeConflict(spent.hash, tx); - // Emit the _removed_ transaction. - self.emit('conflict', tx, info); + // Spender was not removed, the current + // transaction is not elligible to be added. + if (!conflict) + return false; - next(); - }); - }); - }); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, true); - }); -}; + this.logger.warning('Removed conflict: %s.', conflict.tx.rhash); + + // Emit the _removed_ transaction. + this.emit('conflict', conflict.tx, conflict.info); + } + + return true; +}); /** * Attempt to resolve orphans for an output. * @private * @param {TX} tx * @param {Number} index - * @param {Function} callback + * @returns {Promise} */ -TXDB.prototype._resolveOrphans = function _resolveOrphans(tx, index, callback) { - var self = this; +TXDB.prototype.resolveOrphans = co(function* resolveOrphans(tx, index) { var hash = tx.hash('hex'); - var coin; + var i, orphans, coin, input, orphan, key; - this._getOrphans(hash, index, function(err, orphans) { - if (err) - return callback(err); + orphans = yield this.getOrphans(hash, index); - if (!orphans) - return callback(null, false); + if (!orphans) + return false; - self.del(layout.o(hash, index)); + this.del(layout.o(hash, index)); - coin = bcoin.coin.fromTX(tx, index); + coin = Coin.fromTX(tx, index); - // Add input to orphan - utils.forEachSerial(orphans, function(item, next) { - var input = item[0]; - var orphan = item[1]; + // Add input to orphan + for (i = 0; i < orphans.length; i++) { + orphan = orphans[i]; + input = orphan.input; + tx = orphan.tx; - // Probably removed by some other means. - if (!orphan) - return next(); + // Probably removed by some other means. + if (!tx) + continue; - orphan.inputs[input.index].coin = coin; + tx.inputs[input.index].coin = coin; - assert(orphan.inputs[input.index].prevout.hash === hash); - assert(orphan.inputs[input.index].prevout.index === index); + assert(tx.inputs[input.index].prevout.hash === hash); + assert(tx.inputs[input.index].prevout.index === index); - // Verify that input script is correct, if not - add - // output to unspent and remove orphan from storage - if (!self.options.verify || orphan.verifyInput(input.index)) { - self.put(layout.d(input.hash, input.index), coin.toRaw()); - return callback(null, true); - } + // Verify that input script is correct, if not - add + // output to unspent and remove orphan from storage + if (!this.options.verify || (yield tx.verifyInputAsync(input.index))) { + key = layout.d(input.hash, input.index); + this.put(key, coin.toRaw()); + return true; + } - self._lazyRemove(orphan, next); - }, function(err) { - if (err) - return callback(err); + yield this.lazyRemove(tx); + } - // Just going to be added again outside. - self.balance.sub(coin); + // Just going to be added again outside. + this.balance.sub(coin); - callback(null, false); - }); - }); -}; + return false; +}); /** - * Add transaction, runs _confirm (separate batch) and - * verify (separate batch for double spenders). + * Add transaction, runs `confirm()` and `verify()`. + * @param {TX} tx + * @param {PathInfo} info + * @returns {Promise} + */ + +TXDB.prototype.add = co(function* add(tx) { + var info = yield this.getPathInfo(tx); + var result; + + this.wallet.start(); + + try { + result = yield this._add(tx, info); + } catch (e) { + this.wallet.drop(); + throw e; + } + + yield this.wallet.commit(); + + return result; +}); + +/** + * Add transaction without a lock. * @private * @param {TX} tx * @param {PathInfo} info - * @param {Function} callback + * @returns {Promise} */ -TXDB.prototype.add = function add(tx, info, callback) { - var self = this; - var hash, i, path, account; - - callback = this._lock(add, [tx, info, callback]); - - if (!callback) - return; +TXDB.prototype._add = co(function* add(tx, info) { + var hash, path, account; + var i, result, input, output, coin; + var prevout, key, address, spender, orphans; assert(!tx.mutable, 'Cannot add mutable TX to wallet.'); // Attempt to confirm tx before adding it. - this._confirm(tx, info, function(err, existing) { - if (err) - return callback(err); + result = yield this.confirm(tx, info); - // Ignore if we already have this tx. - if (existing) - return callback(null, true, info); + // Ignore if we already have this tx. + if (result) + return true; - self._verify(tx, info, function(err, result) { - if (err) - return callback(err); + // Verify and get coins. + // This potentially removes double-spenders. + result = yield this.verify(tx, info); - if (!result) - return callback(null, result, info); + if (!result) + return false; - hash = tx.hash('hex'); + hash = tx.hash('hex'); - self.start(); - self.put(layout.t(hash), tx.toExtended()); + this.put(layout.t(hash), tx.toExtended()); - if (tx.ts === 0) - self.put(layout.p(hash), DUMMY); - else - self.put(layout.h(tx.height, hash), DUMMY); + if (tx.ts === 0) + this.put(layout.p(hash), DUMMY); + else + this.put(layout.h(tx.height, hash), DUMMY); - self.put(layout.m(tx.ps, hash), DUMMY); + this.put(layout.m(tx.ps, hash), DUMMY); - for (i = 0; i < info.accounts.length; i++) { - account = info.accounts[i]; - self.put(layout.T(account, hash), DUMMY); - if (tx.ts === 0) - self.put(layout.P(account, hash), DUMMY); - else - self.put(layout.H(account, tx.height, hash), DUMMY); - self.put(layout.M(account, tx.ps, hash), DUMMY); + for (i = 0; i < info.accounts.length; i++) { + account = info.accounts[i]; + + this.put(layout.T(account, hash), DUMMY); + + if (tx.ts === 0) + this.put(layout.P(account, hash), DUMMY); + else + this.put(layout.H(account, tx.height, hash), DUMMY); + + this.put(layout.M(account, tx.ps, hash), DUMMY); + } + + // Consume unspent money or add orphans + if (!tx.isCoinbase()) { + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + prevout = input.prevout; + + address = input.getHash('hex'); + path = info.getPath(address); + + // Only bother if this input is ours. + if (!path) + continue; + + key = prevout.hash + prevout.index; + + // s[outpoint-key] -> [spender-hash]|[spender-input-index] + spender = Outpoint.fromTX(tx, i).toRaw(); + this.put(layout.s(prevout.hash, prevout.index), spender); + + // Add orphan, if no parent transaction is yet known + if (!input.coin) { + yield this.addOrphan(prevout, spender); + continue; } - // Consume unspent money or add orphans - utils.forEachSerial(tx.inputs, function(input, next, i) { - var prevout = input.prevout; - var key, address, spender; + this.del(layout.c(prevout.hash, prevout.index)); + this.del(layout.C(path.account, prevout.hash, prevout.index)); + this.put(layout.d(hash, i), input.coin.toRaw()); + this.balance.sub(input.coin); - if (tx.isCoinbase()) - return next(); + this.coinCache.remove(key); + } + } - address = input.getHash('hex'); - path = info.getPath(address); + // Add unspent outputs or resolve orphans + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + address = output.getHash('hex'); + key = hash + i; - // Only bother if this input is ours. - if (!path) - return next(); + path = info.getPath(address); - key = prevout.hash + prevout.index; + // Do not add unspents for outputs that aren't ours. + if (!path) + continue; - // s/[outpoint-key] -> [spender-hash]|[spender-input-index] - spender = bcoin.outpoint.fromTX(tx, i).toRaw(); - self.put(layout.s(prevout.hash, prevout.index), spender); + orphans = yield this.resolveOrphans(tx, i); - // Add orphan, if no parent transaction is yet known - if (!input.coin) - return self._addOrphan(prevout, spender, next); + if (orphans) + continue; - self.del(layout.c(prevout.hash, prevout.index)); - self.del(layout.C(path.account, prevout.hash, prevout.index)); - self.put(layout.d(hash, i), input.coin.toRaw()); - self.balance.sub(input.coin); + coin = Coin.fromTX(tx, i); - self.coinCache.remove(key); + this.balance.add(coin); - next(); - }, function(err) { - if (err) { - self.drop(); - return callback(err); - } + coin = coin.toRaw(); - // Add unspent outputs or resolve orphans - utils.forEachSerial(tx.outputs, function(output, next, i) { - var address = output.getHash('hex'); - var key = hash + i; - var coin; + this.put(layout.c(hash, i), coin); + this.put(layout.C(path.account, hash, i), DUMMY); - path = info.getPath(address); + this.coinCache.set(key, coin); + } - // Do not add unspents for outputs that aren't ours. - if (!path) - return next(); + // Clear any locked coins to free up memory. + this.unlockTX(tx); - self._resolveOrphans(tx, i, function(err, orphans) { - if (err) - return next(err); + this.emit('tx', tx, info); - if (orphans) - return next(); + if (tx.ts !== 0) + this.emit('confirmed', tx, info); - coin = bcoin.coin.fromTX(tx, i); - self.balance.add(coin); - coin = coin.toRaw(); - - self.put(layout.c(hash, i), coin); - self.put(layout.C(path.account, hash, i), DUMMY); - - self.coinCache.set(key, coin); - - next(); - }); - }, function(err) { - if (err) { - self.drop(); - return callback(err); - } - - self.commit(function(err) { - if (err) - return callback(err); - - // Clear any locked coins to free up memory. - self.unlockTX(tx); - - self.emit('tx', tx, info); - - if (tx.ts !== 0) - self.emit('confirmed', tx, info); - - callback(null, true, info); - }); - }); - }); - }); - }); -}; + return true; +}); /** * Remove spenders that have not been confirmed. We do this in the @@ -793,292 +704,279 @@ TXDB.prototype.add = function add(tx, info, callback) { * @private * @param {Hash} hash * @param {TX} ref - Reference tx, the tx that double-spent. - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -TXDB.prototype._removeConflict = function _removeConflict(hash, ref, callback) { - var self = this; +TXDB.prototype.removeConflict = co(function* removeConflict(hash, ref) { + var tx = yield this.getTX(hash); + var info; - this.getTX(hash, function(err, tx) { - if (err) - return callback(err); + if (!tx) + throw new Error('Could not find spender.'); - if (!tx) - return callback(new Error('Could not find spender.')); + if (tx.ts !== 0) { + // If spender is confirmed and replacement + // is not confirmed, do nothing. + if (ref.ts === 0) + return; - if (tx.ts !== 0) { - // If spender is confirmed and replacement - // is not confirmed, do nothing. - if (ref.ts === 0) - return callback(); + // If both are confirmed but replacement + // is older than spender, do nothing. + if (ref.height < tx.height) + return; + } else { + // If spender is unconfirmed and replacement + // is confirmed, do nothing. + if (ref.ts !== 0) + return; - // If both are confirmed but replacement - // is older than spender, do nothing. - if (ref.ts < tx.ts) - return callback(); - } else { - // If spender is unconfirmed and replacement - // is confirmed, do nothing. - if (ref.ts !== 0) - return callback(); + // If both are unconfirmed but replacement + // is older than spender, do nothing. + if (ref.ps < tx.ps) + return; + } - // If both are unconfirmed but replacement - // is older than spender, do nothing. - if (ref.ps < tx.ps) - return callback(); - } + info = yield this.removeRecursive(tx); - self._removeRecursive(tx, function(err, result, info) { - if (err) - return callback(err); - callback(null, tx, info); - }); - }); -}; + return new Conflict(tx, info); +}); /** * Remove a transaction and recursively * remove all of its spenders. * @private * @param {TX} tx - Transaction to be removed. - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -TXDB.prototype._removeRecursive = function _removeRecursive(tx, callback) { - var self = this; +TXDB.prototype.removeRecursive = co(function* removeRecursive(tx, removed) { var hash = tx.hash('hex'); + var i, spent, stx, info; - utils.forEachSerial(tx.outputs, function(output, next, i) { - self.isSpent(hash, i, function(err, spent) { - if (err) - return next(err); + if (!removed) + removed = {}; - if (!spent) - return next(); + for (i = 0; i < tx.outputs.length; i++) { + spent = yield this.isSpent(hash, i); - // Remove all of the spender's spenders first. - self.getTX(spent.hash, function(err, tx) { - if (err) - return next(err); + if (!spent) + continue; - if (!tx) - return next(new Error('Could not find spender.')); + if (removed[spent.hash]) + continue; - self._removeRecursive(tx, next); - }); - }); - }, function(err) { - if (err) - return callback(err); + removed[spent.hash] = true; - self.start(); + // Remove all of the spender's spenders first. + stx = yield this.getTX(spent.hash); - // Remove the spender. - self._lazyRemove(tx, function(err, result, info) { - if (err) { - self.drop(); - return callback(err); - } + if (!stx) + throw new Error('Could not find spender.'); - self.commit(function(err) { - if (err) - return callback(err); - callback(null, result, info); - }); - }); - }); -}; + yield this.removeRecursive(stx, removed); + } + + // Remove the spender. + info = yield this.lazyRemove(tx); + + if (!info) + throw new Error('Cannot remove spender.'); + + return info; +}); /** * Test an entire transaction to see * if any of its outpoints are a double-spend. * @param {TX} tx - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -TXDB.prototype.isDoubleSpend = function isDoubleSpend(tx, callback) { - var self = this; +TXDB.prototype.isDoubleSpend = co(function* isDoubleSpend(tx) { + var i, input, prevout, spent; - utils.everySerial(tx.inputs, function(input, next) { - var prevout = input.prevout; - self.isSpent(prevout.hash, prevout.index, function(err, spent) { - if (err) - return next(err); - return next(null, !spent); - }); - }, function(err, result) { - if (err) - return callback(err); - callback(null, !result); - }); -}; + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + prevout = input.prevout; + spent = yield this.isSpent(prevout.hash, prevout.index); + if (spent) + return true; + } + + return false; +}); /** * Test a whether a coin has been spent. * @param {Hash} hash * @param {Number} index - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -TXDB.prototype.isSpent = function isSpent(hash, index, callback) { +TXDB.prototype.isSpent = co(function* isSpent(hash, index) { var key = layout.s(hash, index); - this.fetch(key, function(data) { - return bcoin.outpoint.fromRaw(data); - }, callback); -}; + var data = yield this.get(key); + + if (!data) + return; + + return Outpoint.fromRaw(data); +}); /** * Attempt to confirm a transaction. * @private * @param {TX} tx * @param {AddressMap} info - * @param {Function} callback - Returns [Error, Boolean]. `false` if + * @returns {Promise} - Returns Boolean. `false` if * the transaction should be added to the database, `true` if the * transaction was confirmed, or should be ignored. */ -TXDB.prototype._confirm = function _confirm(tx, info, callback) { - var self = this; +TXDB.prototype.confirm = co(function* confirm(tx, info) { var hash = tx.hash('hex'); - var i, account; + var i, account, existing, output, coin; + var address, key; - this.getTX(hash, function(err, existing) { - if (err) - return callback(err); + existing = yield this.getTX(hash); - // Haven't seen this tx before, add it. - if (!existing) - return callback(null, false, info); + // Haven't seen this tx before, add it. + if (!existing) + return false; - // Existing tx is already confirmed. Ignore. - if (existing.ts !== 0) - return callback(null, true, info); + // Existing tx is already confirmed. Ignore. + if (existing.ts !== 0) + return true; - // The incoming tx won't confirm the - // existing one anyway. Ignore. - if (tx.ts === 0) - return callback(null, true, info); + // The incoming tx won't confirm the + // existing one anyway. Ignore. + if (tx.ts === 0) + return true; - // Tricky - update the tx and coin in storage, - // and remove pending flag to mark as confirmed. - assert(tx.height >= 0); + // Tricky - update the tx and coin in storage, + // and remove pending flag to mark as confirmed. + assert(tx.height >= 0); - // Clear any locked coins to free up memory. - self.unlockTX(tx); + // Clear any locked coins to free up memory. + this.unlockTX(tx); - // Save the original received time. - tx.ps = existing.ps; + // Save the original received time. + tx.ps = existing.ps; - self.start(); + this.put(layout.t(hash), tx.toExtended()); - self.put(layout.t(hash), tx.toExtended()); + this.del(layout.p(hash)); + this.put(layout.h(tx.height, hash), DUMMY); - self.del(layout.p(hash)); - self.put(layout.h(tx.height, hash), DUMMY); + for (i = 0; i < info.accounts.length; i++) { + account = info.accounts[i]; + this.del(layout.P(account, hash)); + this.put(layout.H(account, tx.height, hash), DUMMY); + } - for (i = 0; i < info.accounts.length; i++) { - account = info.accounts[i]; - self.del(layout.P(account, hash)); - self.put(layout.H(account, tx.height, hash), DUMMY); + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + address = output.getHash('hex'); + key = hash + i; + + // Only update coins if this output is ours. + if (!info.hasPath(address)) + continue; + + coin = yield this.getCoin(hash, i); + + // Update spent coin. + if (!coin) { + yield this.updateSpentCoin(tx, i); + continue; } - utils.forEachSerial(tx.outputs, function(output, next, i) { - var address = output.getHash('hex'); - var key = hash + i; + this.balance.confirm(coin.value); - // Only update coins if this output is ours. - if (!info.hasPath(address)) - return next(); + coin.height = tx.height; + coin = coin.toRaw(); - self.getCoin(hash, i, function(err, coin) { - if (err) - return next(err); + this.put(layout.c(hash, i), coin); - // Update spent coin. - if (!coin) - return self.updateSpentCoin(tx, i, next); + this.coinCache.set(key, coin); + } - self.balance.confirm(coin.value); + this.emit('tx', tx, info); + this.emit('confirmed', tx, info); - coin.height = tx.height; - coin = coin.toRaw(); - - self.put(layout.c(hash, i), coin); - - self.coinCache.set(key, coin); - - next(); - }); - }, function(err) { - if (err) { - self.drop(); - return callback(err); - } - - self.commit(function(err) { - if (err) - return callback(err); - - self.emit('tx', tx, info); - self.emit('confirmed', tx, info); - - callback(null, true, info); - }); - }); - }); -}; + return true; +}); /** * Remove a transaction from the database. Disconnect inputs. * @param {Hash} hash - * @param {Function} callback - Returns [Error]. + * @returns {Promise} */ -TXDB.prototype.remove = function remove(hash, callback, force) { - callback = this._lock(remove, [hash, callback], force); +TXDB.prototype.remove = co(function* remove(hash) { + var result; - if (!callback) + this.wallet.start(); + + try { + result = yield this._remove(hash); + } catch (e) { + this.wallet.drop(); + throw e; + } + + yield this.wallet.commit(); + + return result; +}); + +/** + * Remove a transaction without a lock. + * @private + * @param {Hash} hash + * @returns {Promise} + */ + +TXDB.prototype._remove = co(function* remove(hash) { + var tx = yield this.getTX(hash); + var info; + + if (!tx) return; - this._removeRecursive(hash, function(err, result, info) { - if (err) - return callback(err); + info = yield this.removeRecursive(tx); - callback(null, !!result, info); - }); -}; + if (!info) + return; + + return info; +}); /** * Remove a transaction from the database, but do not * look up the transaction. Use the passed-in transaction * to disconnect. * @param {TX} tx - * @param {Function} callback - Returns [Error]. + * @returns {Promise} */ -TXDB.prototype._lazyRemove = function lazyRemove(tx, callback) { - var self = this; - this.getInfo(tx, function(err, info) { - if (err) - return callback(err); +TXDB.prototype.lazyRemove = co(function* lazyRemove(tx) { + var info = yield this.getPathInfo(tx); + if (!info) + return; - if (!info) - return callback(null, false); - - self._remove(tx, info, callback); - }); -}; + return yield this.__remove(tx, info); +}); /** * Remove a transaction from the database. Disconnect inputs. * @private * @param {TX} tx * @param {AddressMap} info - * @param {Function} callback - Returns [Error]. + * @returns {Promise} */ -TXDB.prototype._remove = function remove(tx, info, callback) { - var self = this; +TXDB.prototype.__remove = co(function* remove(tx, info) { var hash = tx.hash('hex'); var i, path, account, key, prevout; var address, input, output, coin; @@ -1094,17 +992,19 @@ TXDB.prototype._remove = function remove(tx, info, callback) { for (i = 0; i < info.accounts.length; i++) { account = info.accounts[i]; + this.del(layout.T(account, hash)); + if (tx.ts === 0) this.del(layout.P(account, hash)); else this.del(layout.H(account, tx.height, hash)); + this.del(layout.M(account, tx.ps, hash)); } - this.fillHistory(tx, function(err) { - if (err) - return callback(err); + if (!tx.isCoinbase()) { + yield this.fillHistory(tx); for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; @@ -1112,9 +1012,6 @@ TXDB.prototype._remove = function remove(tx, info, callback) { prevout = input.prevout; address = input.getHash('hex'); - if (tx.isCoinbase()) - break; - if (!input.coin) continue; @@ -1123,106 +1020,106 @@ TXDB.prototype._remove = function remove(tx, info, callback) { if (!path) continue; - self.balance.add(input.coin); + this.balance.add(input.coin); coin = input.coin.toRaw(); - self.put(layout.c(prevout.hash, prevout.index), coin); - self.put(layout.C(path.account, prevout.hash, prevout.index), DUMMY); - self.del(layout.d(hash, i)); - self.del(layout.s(prevout.hash, prevout.index)); - self.del(layout.o(prevout.hash, prevout.index)); + this.put(layout.c(prevout.hash, prevout.index), coin); + this.put(layout.C(path.account, prevout.hash, prevout.index), DUMMY); + this.del(layout.d(hash, i)); + this.del(layout.s(prevout.hash, prevout.index)); + this.del(layout.o(prevout.hash, prevout.index)); - self.coinCache.set(key, coin); + this.coinCache.set(key, coin); } + } - for (i = 0; i < tx.outputs.length; i++) { - output = tx.outputs[i]; - key = hash + i; - address = output.getHash('hex'); + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + key = hash + i; + address = output.getHash('hex'); - path = info.getPath(address); + path = info.getPath(address); - if (!path) - continue; + if (!path) + continue; - coin = bcoin.coin.fromTX(tx, i); + coin = Coin.fromTX(tx, i); - self.balance.sub(coin); + this.balance.sub(coin); - self.del(layout.c(hash, i)); - self.del(layout.C(path.account, hash, i)); + this.del(layout.c(hash, i)); + this.del(layout.C(path.account, hash, i)); - self.coinCache.remove(key); - } + this.coinCache.remove(key); + } - self.emit('remove tx', tx, info); + this.emit('remove tx', tx, info); - callback(null, true, info); - }); -}; + return info; +}); /** * Unconfirm a transaction. This is usually necessary after a reorg. * @param {Hash} hash - * @param {Function} callback + * @returns {Promise} */ -TXDB.prototype.unconfirm = function unconfirm(hash, callback, force) { - var self = this; +TXDB.prototype.unconfirm = co(function* unconfirm(hash) { + var result; - callback = this._lock(unconfirm, [hash, callback], force); + this.wallet.start(); - if (!callback) - return; + try { + result = yield this._unconfirm(hash); + } catch (e) { + this.wallet.drop(); + throw e; + } - this.getTX(hash, function(err, tx) { - if (err) - return callback(err); + yield this.wallet.commit(); - if (!tx) - return callback(null, false); + return result; +}); - self.getInfo(tx, function(err, info) { - if (err) - return callback(err); +/** + * Unconfirm a transaction without a lock. + * @private + * @param {Hash} hash + * @returns {Promise} + */ - if (!info) - return callback(null, false); +TXDB.prototype._unconfirm = co(function* unconfirm(hash) { + var tx = yield this.getTX(hash); + var info, result; - self.start(); + if (!tx) + return false; - self._unconfirm(tx, info, function(err, result, info) { - if (err) { - self.drop(); - return callback(err); - } + info = yield this.getPathInfo(tx); - self.commit(function(err) { - if (err) - return callback(err); - callback(null, result, info); - }); - }); - }); - }); -}; + if (!info) + return false; + + result = yield this.__unconfirm(tx, info); + + return result; +}); /** * Unconfirm a transaction. This is usually necessary after a reorg. * @param {Hash} hash * @param {AddressMap} info - * @param {Function} callback + * @returns {Promise} */ -TXDB.prototype._unconfirm = function unconfirm(tx, info, callback) { - var self = this; +TXDB.prototype.__unconfirm = co(function* unconfirm(tx, info) { var hash = tx.hash('hex'); var height = tx.height; - var i, account; + var i, account, output, key, coin; if (height === -1) - return callback(null, false, info); + return; tx.height = -1; tx.ts = 0; @@ -1240,35 +1137,31 @@ TXDB.prototype._unconfirm = function unconfirm(tx, info, callback) { this.del(layout.H(account, height, hash)); } - utils.forEachSerial(tx.outputs, function(output, next, i) { - var key = hash + i; - self.getCoin(hash, i, function(err, coin) { - if (err) - return next(err); + for (i = 0; i < tx.outputs.length; i++) { + output = tx.outputs[i]; + key = hash + i; + coin = yield this.getCoin(hash, i); - // Update spent coin. - if (!coin) - return self.updateSpentCoin(tx, i, next); + // Update spent coin. + if (!coin) { + yield this.updateSpentCoin(tx, i); + continue; + } - self.balance.unconfirm(coin.value); - coin.height = tx.height; - coin = coin.toRaw(); + this.balance.unconfirm(coin.value); - self.put(layout.c(hash, i), coin); + coin.height = tx.height; + coin = coin.toRaw(); - self.coinCache.set(key, coin); + this.put(layout.c(hash, i), coin); - next(); - }); - }, function(err) { - if (err) - return callback(err); + this.coinCache.set(key, coin); + } - self.emit('unconfirmed', tx, info); + this.emit('unconfirmed', tx, info); - callback(null, true, info); - }); -}; + return info; +}); /** * Lock all coins in a transaction. @@ -1368,7 +1261,7 @@ TXDB.prototype.getLocked = function getLocked() { key = keys[i]; hash = key.slice(0, 64); index = +key.slice(64); - outpoint = new bcoin.outpoint(hash, index); + outpoint = new Outpoint(hash, index); outpoints.push(outpoint); } @@ -1378,16 +1271,11 @@ TXDB.prototype.getLocked = function getLocked() { /** * Get hashes of all transactions in the database. * @param {Number?} account - * @param {Function} callback - Returns [Error, {@link Hash}[]]. + * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getHistoryHashes = function getHistoryHashes(account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - - this.iterate({ +TXDB.prototype.getHistoryHashes = function getHistoryHashes(account) { + return this.keys({ gte: account != null ? layout.T(account, constants.NULL_HASH) : layout.t(constants.NULL_HASH), @@ -1402,22 +1290,17 @@ TXDB.prototype.getHistoryHashes = function getHistoryHashes(account, callback) { key = layout.tt(key); return key; } - }, callback); + }); }; /** * Get hashes of all unconfirmed transactions in the database. * @param {Number?} account - * @param {Function} callback - Returns [Error, {@link Hash}[]]. + * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - - this.iterate({ +TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account) { + return this.keys({ gte: account != null ? layout.P(account, constants.NULL_HASH) : layout.p(constants.NULL_HASH), @@ -1432,22 +1315,17 @@ TXDB.prototype.getUnconfirmedHashes = function getUnconfirmedHashes(account, cal key = layout.pp(key); return key; } - }, callback); + }); }; /** * Get all coin hashes in the database. * @param {Number?} account - * @param {Function} callback - Returns [Error, {@link Hash}[]]. + * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getCoinHashes = function getCoinHashes(account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - - this.iterate({ +TXDB.prototype.getOutpoints = function getOutpoints(account) { + return this.keys({ gte: account != null ? layout.C(account, constants.NULL_HASH, 0) : layout.c(constants.NULL_HASH, 0), @@ -1457,12 +1335,12 @@ TXDB.prototype.getCoinHashes = function getCoinHashes(account, callback) { parse: function(key) { if (account != null) { key = layout.Cc(key); - return [key[1], key[2]]; + return new Outpoint(key[1], key[2]); } key = layout.cc(key); - return key; + return new Outpoint(key[0], key[1]); } - }, callback); + }); }; /** @@ -1473,14 +1351,13 @@ TXDB.prototype.getCoinHashes = function getCoinHashes(account, callback) { * @param {Number} options.end - End height. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. - * @param {Function} callback - Returns [Error, {@link Hash}[]]. + * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, options, callback) { +TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, options) { var start, end; - if (typeof account !== 'number') { - callback = options; + if (account && typeof account === 'object') { options = account; account = null; } @@ -1488,7 +1365,7 @@ TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, opt start = options.start || 0; end = options.end || 0xffffffff; - this.iterate({ + return this.keys({ gte: account != null ? layout.H(account, start, constants.NULL_HASH) : layout.h(start, constants.NULL_HASH), @@ -1505,17 +1382,17 @@ TXDB.prototype.getHeightRangeHashes = function getHeightRangeHashes(account, opt key = layout.hh(key); return key[1]; } - }, callback); + }); }; /** * Get TX hashes by height. * @param {Number} height - * @param {Function} callback - Returns [Error, {@link Hash}[]]. + * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getHeightHashes = function getHeightHashes(height, callback) { - this.getHeightRangeHashes({ start: height, end: height }, callback); +TXDB.prototype.getHeightHashes = function getHeightHashes(height) { + return this.getHeightRangeHashes({ start: height, end: height }); }; /** @@ -1526,21 +1403,21 @@ TXDB.prototype.getHeightHashes = function getHeightHashes(height, callback) { * @param {Number} options.end - End height. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. - * @param {Function} callback - Returns [Error, {@link Hash}[]]. + * @returns {Promise} - Returns {@link Hash}[]. */ -TXDB.prototype.getRangeHashes = function getRangeHashes(account, options, callback) { +TXDB.prototype.getRangeHashes = function getRangeHashes(account, options) { var start, end; - if (typeof account === 'function') { - callback = account; + if (account && typeof account === 'object') { + options = account; account = null; } start = options.start || 0; end = options.end || 0xffffffff; - this.iterate({ + return this.keys({ gte: account != null ? layout.M(account, start, constants.NULL_HASH) : layout.m(start, constants.NULL_HASH), @@ -1557,7 +1434,7 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(account, options, callba key = layout.mm(key); return key[1]; } - }, callback); + }); }; /** @@ -1568,529 +1445,423 @@ TXDB.prototype.getRangeHashes = function getRangeHashes(account, options, callba * @param {Number} options.end - End time. * @param {Number?} options.limit - Max number of records. * @param {Boolean?} options.reverse - Reverse order. - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getRange = function getRange(account, options, callback) { - var self = this; +TXDB.prototype.getRange = co(function* getRange(account, options) { var txs = []; + var i, hashes, hash, tx; - if (typeof account === 'function') { - callback = account; + if (account && typeof account === 'object') { + options = account; account = null; } - this.getRangeHashes(account, options, function(err, hashes) { - if (err) - return callback(err); + hashes = yield this.getRangeHashes(account, options); - utils.forEachSerial(hashes, function(hash, next) { - self.getTX(hash, function(err, tx) { - if (err) - return callback(err); + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + tx = yield this.getTX(hash); - if (!tx) - return next(); + if (!tx) + continue; - txs.push(tx); + txs.push(tx); + } - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, txs); - }); - }); -}; + return txs; +}); /** * Get last N transactions. * @param {Number?} account * @param {Number} limit - Max number of transactions. - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getLast = function getLast(account, limit, callback) { - if (typeof limit === 'function') { - callback = limit; - limit = account; - account = null; - } - - this.getRange(account, { +TXDB.prototype.getLast = function getLast(account, limit) { + return this.getRange(account, { start: 0, end: 0xffffffff, reverse: true, limit: limit - }, callback); + }); }; /** * Get all transactions. * @param {Number?} account - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getHistory = function getHistory(account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - +TXDB.prototype.getHistory = function getHistory(account) { // Slow case if (account != null) - return this.getAccountHistory(account, callback); + return this.getAccountHistory(account); // Fast case - this.iterate({ + return this.values({ gte: layout.t(constants.NULL_HASH), lte: layout.t(constants.HIGH_HASH), - keys: false, - values: true, - parse: function(key, value) { - return bcoin.tx.fromExtended(value); - } - }, callback); + parse: TX.fromExtended + }); }; /** * Get all account transactions. * @param {Number?} account - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getAccountHistory = function getAccountHistory(account, callback) { - var self = this; +TXDB.prototype.getAccountHistory = co(function* getAccountHistory(account) { var txs = []; + var i, hashes, hash, tx; - if (typeof account === 'function') { - callback = account; - account = null; + hashes = yield this.getHistoryHashes(account); + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + tx = yield this.getTX(hash); + + if (!tx) + continue; + + txs.push(tx); } - this.getHistoryHashes(account, function(err, hashes) { - if (err) - return callback(err); - - utils.forEachSerial(hashes, function(hash, next) { - self.getTX(hash, function(err, tx) { - if (err) - return callback(err); - - if (!tx) - return next(); - - txs.push(tx); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, sortTX(txs)); - }); - }); -}; + return sortTX(txs); +}); /** * Get unconfirmed transactions. * @param {Number?} account - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * @returns {Promise} - Returns {@link TX}[]. */ -TXDB.prototype.getUnconfirmed = function getUnconfirmed(account, callback) { - var self = this; +TXDB.prototype.getUnconfirmed = co(function* getUnconfirmed(account) { var txs = []; + var i, hashes, hash, tx; - if (typeof account === 'function') { - callback = account; - account = null; + hashes = yield this.getUnconfirmedHashes(account); + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + tx = yield this.getTX(hash); + + if (!tx) + continue; + + txs.push(tx); } - this.getUnconfirmedHashes(account, function(err, hashes) { - if (err) - return callback(err); - - utils.forEachSerial(hashes, function(hash, next) { - self.getTX(hash, function(err, tx) { - if (err) - return callback(err); - - if (!tx) - return next(); - - txs.push(tx); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, sortTX(txs)); - }); - }); -}; + return sortTX(txs); +}); /** * Get coins. * @param {Number?} account - * @param {Function} callback - Returns [Error, {@link Coin}[]]. + * @returns {Promise} - Returns {@link Coin}[]. */ -TXDB.prototype.getCoins = function getCoins(account, callback) { +TXDB.prototype.getCoins = function getCoins(account) { var self = this; - if (typeof account === 'function') { - callback = account; - account = null; - } - // Slow case if (account != null) - return this.getAccountCoins(account, callback); + return this.getAccountCoins(account); // Fast case - this.iterate({ - gte: layout.c(constants.NULL_HASH, 0), + return this.range({ + gte: layout.c(constants.NULL_HASH, 0x00000000), lte: layout.c(constants.HIGH_HASH, 0xffffffff), - values: true, parse: function(key, value) { var parts = layout.cc(key); var hash = parts[0]; var index = parts[1]; - var coin = bcoin.coin.fromRaw(value); + var coin = Coin.fromRaw(value); coin.hash = hash; coin.index = index; key = hash + index; self.coinCache.set(key, value); return coin; } - }, callback); + }); }; /** * Get coins by account. * @param {Number} account - * @param {Function} callback - Returns [Error, {@link Coin}[]]. + * @returns {Promise} - Returns {@link Coin}[]. */ -TXDB.prototype.getAccountCoins = function getCoins(account, callback) { - var self = this; +TXDB.prototype.getAccountCoins = co(function* getCoins(account) { + var prevout = yield this.getOutpoints(account); var coins = []; + var i, op, coin; - this.getCoinHashes(account, function(err, hashes) { - if (err) - return callback(err); + for (i = 0; i < prevout.length; i++) { + op = prevout[i]; + coin = yield this.getCoin(op.hash, op.index); - utils.forEachSerial(hashes, function(key, next) { - self.getCoin(key[0], key[1], function(err, coin) { - if (err) - return callback(err); + if (!coin) + continue; - if (!coin) - return next(); + coins.push(coin); + } - coins.push(coin); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, coins); - }); - }); -}; + return coins; +}); /** * Fill a transaction with coins (all historical coins). * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -TXDB.prototype.fillHistory = function fillHistory(tx, callback) { +TXDB.prototype.fillHistory = function fillHistory(tx) { var hash; - if (tx.isCoinbase()) { - callback = utils.asyncify(callback); - return callback(null, tx); - } + if (tx.isCoinbase()) + return Promise.resolve(tx); hash = tx.hash('hex'); - this.iterate({ - gte: layout.d(hash, 0), + return this.range({ + gte: layout.d(hash, 0x00000000), lte: layout.d(hash, 0xffffffff), - values: true, parse: function(key, value) { var index = layout.dd(key)[1]; - var coin = bcoin.coin.fromRaw(value); + var coin = Coin.fromRaw(value); var input = tx.inputs[index]; coin.hash = input.prevout.hash; coin.index = input.prevout.index; input.coin = coin; } - }, function(err) { - if (err) - return callback(err); - callback(null, tx); }); }; /** * Fill a transaction with coins. * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -TXDB.prototype.fillCoins = function fillCoins(tx, callback) { - var self = this; +TXDB.prototype.fillCoins = co(function* fillCoins(tx) { + var i, input, prevout, coin; - if (tx.isCoinbase()) { - callback = utils.asyncify(callback); - return callback(null, tx); - } + if (tx.isCoinbase()) + return tx; - utils.forEachSerial(tx.inputs, function(input, next) { - var prevout = input.prevout; + for (i = 0; i < tx.inputs.length; i++) { + input = tx.inputs[i]; + prevout = input.prevout; if (input.coin) - return next(); + continue; - self.getCoin(prevout.hash, prevout.index, function(err, coin) { - if (err) - return callback(err); + coin = yield this.getCoin(prevout.hash, prevout.index); - if (coin) - input.coin = coin; + if (coin) + input.coin = coin; + } - next(); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, tx); - }); -}; + return tx; +}); /** * Get transaction. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -TXDB.prototype.getTX = function getTX(hash, callback) { - this.fetch(layout.t(hash), function(tx) { - return bcoin.tx.fromExtended(tx); - }, callback); -}; +TXDB.prototype.getTX = co(function* getTX(hash) { + var tx = yield this.get(layout.t(hash)); + + if (!tx) + return; + + return TX.fromExtended(tx); +}); /** * Get transaction details. * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link TXDetails}]. + * @returns {Promise} - Returns {@link TXDetails}. */ -TXDB.prototype.getDetails = function getDetails(hash, callback) { - var self = this; - this.getTX(hash, function(err, tx) { - if (err) - return callback(err); +TXDB.prototype.getDetails = co(function* getDetails(hash) { + var tx = yield this.getTX(hash); - if (!tx) - return callback(); + if (!tx) + return; - self.toDetails(tx, callback); - }); -}; + return yield this.toDetails(tx); +}); /** * Convert transaction to transaction details. * @param {TX|TX[]} tx - * @param {Function} callback + * @returns {Promise} */ -TXDB.prototype.toDetails = function toDetails(tx, callback) { - var self = this; - var out; +TXDB.prototype.toDetails = co(function* toDetails(tx) { + var i, out, txs, details, info; if (Array.isArray(tx)) { out = []; - return utils.forEachSerial(tx, function(tx, next) { - self.toDetails(tx, function(err, details) { - if (err) - return next(err); + txs = tx; - if (!details) - return next(); + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + details = yield this.toDetails(tx); - out.push(details); - next(); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, out); - }); + if (!details) + continue; + + out.push(details); + } + + return out; } - this.fillHistory(tx, function(err) { - if (err) - return callback(err); + yield this.fillHistory(tx); - self.getInfo(tx, function(err, info) { - if (err) - return callback(err); + info = yield this.getPathInfo(tx); - if (!info) - return callback(new Error('Info not found.')); + if (!info) + throw new Error('Info not found.'); - callback(null, info.toDetails()); - }); - }); -}; + return info.toDetails(); +}); /** * Test whether the database has a transaction. * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -TXDB.prototype.hasTX = function hasTX(hash, callback) { - this.has(layout.t(hash), callback); +TXDB.prototype.hasTX = function hasTX(hash) { + return this.has(layout.t(hash)); }; /** * Get coin. * @param {Hash} hash * @param {Number} index - * @param {Function} callback - Returns [Error, {@link Coin}]. + * @returns {Promise} - Returns {@link Coin}. */ -TXDB.prototype.getCoin = function getCoin(hash, index, callback) { - var self = this; +TXDB.prototype.getCoin = co(function* getCoin(hash, index) { var key = hash + index; - var coin = this.coinCache.get(key); + var data = this.coinCache.get(key); + var coin; - if (coin) { - try { - coin = bcoin.coin.fromRaw(coin); - } catch (e) { - return callback(e); - } + if (data) { + coin = Coin.fromRaw(data); coin.hash = hash; coin.index = index; - return callback(null, coin); + return coin; } - this.fetch(layout.c(hash, index), function(data) { - var coin = bcoin.coin.fromRaw(data); - coin.hash = hash; - coin.index = index; - self.coinCache.set(key, data); - return coin; - }, callback); -}; + data = yield this.get(layout.c(hash, index)); + + if (!data) + return; + + coin = Coin.fromRaw(data); + coin.hash = hash; + coin.index = index; + + this.coinCache.set(key, data); + + return coin; +}); /** * Get spender coin. * @param {Outpoint} spent * @param {Outpoint} prevout - * @param {Function} callback - Returns [Error, {@link Coin}]. + * @returns {Promise} - Returns {@link Coin}. */ -TXDB.prototype.getSpentCoin = function getSpentCoin(spent, prevout, callback) { - this.fetch(layout.d(spent.hash, spent.index), function(data) { - var coin = bcoin.coin.fromRaw(data); - coin.hash = prevout.hash; - coin.index = prevout.index; - return coin; - }, callback); -}; +TXDB.prototype.getSpentCoin = co(function* getSpentCoin(spent, prevout) { + var data = yield this.get(layout.d(spent.hash, spent.index)); + var coin; + + if (!data) + return; + + coin = Coin.fromRaw(data); + coin.hash = prevout.hash; + coin.index = prevout.index; + + return coin; +}); /** * Update spent coin height in storage. * @param {TX} tx - Sending transaction. * @param {Number} index - * @param {Function} callback + * @returns {Promise} */ -TXDB.prototype.updateSpentCoin = function updateSpentCoin(tx, i, callback) { - var self = this; - var prevout = bcoin.outpoint.fromTX(tx, i); - this.isSpent(prevout.hash, prevout.index, function(err, spent) { - if (err) - return callback(err); +TXDB.prototype.updateSpentCoin = co(function* updateSpentCoin(tx, i) { + var prevout = Outpoint.fromTX(tx, i); + var spent = yield this.isSpent(prevout.hash, prevout.index); + var coin; - if (!spent) - return callback(); + if (!spent) + return; - self.getSpentCoin(spent, prevout, function(err, coin) { - if (err) - return callback(err); + coin = yield this.getSpentCoin(spent, prevout); - if (!coin) - return callback(); + if (!coin) + return; - coin.height = tx.height; + coin.height = tx.height; - self.put(layout.d(spent.hash, spent.index), coin.toRaw()); - - callback(); - }); - }); -}; + this.put(layout.d(spent.hash, spent.index), coin.toRaw()); +}); /** * Test whether the database has a transaction. * @param {Hash} hash - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -TXDB.prototype.hasCoin = function hasCoin(hash, index, callback) { +TXDB.prototype.hasCoin = function hasCoin(hash, index) { var key = hash + index; if (this.coinCache.has(key)) - return callback(null, true); + return Promise.resolve(true); - this.has(layout.c(hash, index), callback); + return this.has(layout.c(hash, index)); }; /** * Calculate balance. * @param {Number?} account - * @param {Function} callback - Returns [Error, {@link Balance}]. + * @returns {Promise} - Returns {@link Balance}. */ -TXDB.prototype.getBalance = function getBalance(account, callback) { +TXDB.prototype.getBalance = co(function* getBalance(account) { var self = this; var balance; - if (typeof account === 'function') { - callback = account; - account = null; - } - // Slow case if (account != null) - return this.getAccountBalance(account, callback); + return yield this.getAccountBalance(account); // Really fast case if (this.balance) - return callback(null, this.balance); + return this.balance; // Fast case balance = new Balance(this.wallet); - this.iterate({ - gte: layout.c(constants.NULL_HASH, 0), + yield this.range({ + gte: layout.c(constants.NULL_HASH, 0x00000000), lte: layout.c(constants.HIGH_HASH, 0xffffffff), - values: true, parse: function(key, data) { var parts = layout.cc(key); var hash = parts[0]; @@ -2099,124 +1870,96 @@ TXDB.prototype.getBalance = function getBalance(account, callback) { balance.addRaw(data); self.coinCache.set(ckey, data); } - }, function(err) { - if (err) - return callback(err); - - callback(null, balance); }); -}; + + return balance; +}); /** * Calculate balance by account. * @param {Number} account - * @param {Function} callback - Returns [Error, {@link Balance}]. + * @returns {Promise} - Returns {@link Balance}. */ -TXDB.prototype.getAccountBalance = function getBalance(account, callback) { - var self = this; +TXDB.prototype.getAccountBalance = co(function* getBalance(account) { + var prevout = yield this.getOutpoints(account); var balance = new Balance(this.wallet); - var key, coin; + var i, ckey, key, coin, op, data; - this.getCoinHashes(account, function(err, hashes) { - if (err) - return callback(err); + for (i = 0; i < prevout.length; i++) { + op = prevout[i]; + ckey = op.hash + op.index; + coin = this.coinCache.get(ckey); - utils.forEachSerial(hashes, function(hash, next) { - key = hash[0] + hash[1]; - coin = self.coinCache.get(key); + if (coin) { + balance.addRaw(coin); + continue; + } - if (coin) { - try { - balance.addRaw(coin); - } catch (e) { - return next(e); - } - return next(); - } + key = layout.c(op.hash, op.index); + data = yield this.get(key); - self.get(layout.c(hash[0], hash[1]), function(err, data) { - if (err) - return next(err); + if (!data) + continue; - if (!data) - return next(); + balance.addRaw(data); - try { - balance.addRaw(data); - } catch (e) { - return callback(e); - } - - self.coinCache.set(key, data); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, balance); - }); - }); -}; - -/** - * @param {Number?} account - * @param {Number} age - Age delta (delete transactions older than `now - age`). - * @param {Function} callback - */ - -TXDB.prototype.zap = function zap(account, age, callback) { - var self = this; - - if (typeof age === 'function') { - callback = age; - age = account; - account = null; + this.coinCache.set(ckey, data); } - callback = this._lock(zap, [account, age, callback]); + return balance; +}); - if (!callback) - return; +/** + * Zap pending transactions older than `age`. + * @param {Number?} account + * @param {Number} age - Age delta (delete transactions older than `now - age`). + * @returns {Promise} + */ + +TXDB.prototype.zap = co(function* zap(account, age) { + var i, txs, tx, hash; if (!utils.isUInt32(age)) - return callback(new Error('Age must be a number.')); + throw new Error('Age must be a number.'); - this.getRange(account, { + txs = yield this.getRange(account, { start: 0, - end: bcoin.now() - age - }, function(err, txs) { - if (err) - return callback(err); - - utils.forEachSerial(txs, function(tx, next) { - if (tx.ts !== 0) - return next(); - self.remove(tx.hash('hex'), next, true); - }, callback); + end: utils.now() - age }); -}; + + for (i = 0; i < txs.length; i++) { + tx = txs[i]; + hash = tx.hash('hex'); + + if (tx.ts !== 0) + continue; + + this.wallet.start(); + + try { + yield this._remove(hash); + } catch (e) { + this.wallet.drop(); + throw e; + } + + yield this.wallet.commit(); + } +}); /** * Abandon transaction. * @param {Hash} hash - * @param {Function} callback + * @returns {Promise} */ -TXDB.prototype.abandon = function abandon(hash, callback) { - var self = this; - this.has(layout.p(hash), function(err, result) { - if (err) - return callback(err); - - if (!result) - return callback(new Error('TX not eligible.')); - - self.remove(hash, callback); - }); -}; +TXDB.prototype.abandon = co(function* abandon(hash) { + var result = yield this.has(layout.p(hash)); + if (!result) + throw new Error('TX not eligible.'); + return yield this.remove(hash); +}); /* * Balance @@ -2309,6 +2052,16 @@ function sortCoins(coins) { }); } +function Conflict(tx, info) { + this.tx = tx; + this.info = info; +} + +function Orphan(input, tx) { + this.input = input; + this.tx = tx; +} + /* * Expose */ diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 8b50217d..24430957 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -7,16 +7,28 @@ 'use strict'; -var bcoin = require('../env'); var EventEmitter = require('events').EventEmitter; -var constants = bcoin.constants; +var constants = require('../protocol/constants'); +var Network = require('../protocol/network'); var utils = require('../utils/utils'); +var Locker = require('../utils/locker'); +var co = require('../utils/co'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; +var assert = require('assert'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); var TXDB = require('./txdb'); var Path = require('./path'); +var Address = require('../primitives/address'); +var MTX = require('../primitives/mtx'); +var WalletKey = require('./walletkey'); +var HD = require('../hd/hd'); +var Account = require('./account'); +var MasterKey = require('./masterkey'); +var Input = require('../primitives/input'); +var Output = require('../primitives/output'); +var LRU = require('../utils/lru'); +var PathInfo = require('./pathinfo'); /** * BIP44 Wallet @@ -54,19 +66,24 @@ function Wallet(db, options) { this.db = db; this.network = db.network; this.logger = db.logger; - this.workerPool = db.workerPool; - this.writeLock = new bcoin.locker(this); - this.fundLock = new bcoin.locker(this); + this.readLock = new Locker.Mapped(); + this.writeLock = new Locker(); + this.fundLock = new Locker(); + this.indexCache = new LRU(10000); + this.accountCache = new LRU(10000); + this.pathCache = new LRU(100000); + this.current = null; this.wid = 0; this.id = null; - this.master = null; this.initialized = false; + this.watchOnly = false; this.accountDepth = 0; this.token = constants.ZERO_HASH; this.tokenDepth = 0; - this.tx = new TXDB(this); + this.master = null; + this.txdb = new TXDB(this); this.account = null; if (options) @@ -75,24 +92,6 @@ function Wallet(db, options) { utils.inherits(Wallet, EventEmitter); -/** - * Invoke write mutex lock. - * @private - */ - -Wallet.prototype._lockWrite = function _lockWrite(func, args, force) { - return this.writeLock.lock(func, args, force); -}; - -/** - * Invoke funding mutex lock. - * @private - */ - -Wallet.prototype._lockFund = function _lockFund(func, args, force) { - return this.fundLock.lock(func, args, force); -}; - /** * Inject properties from options object. * @private @@ -103,29 +102,18 @@ Wallet.prototype.fromOptions = function fromOptions(options) { var master = options.master; var id, token; - if (!master) - master = bcoin.hd.fromMnemonic(null, this.network); + if (!MasterKey.isMasterKey(master)) { + if (!master) + master = HD.fromMnemonic(null, this.network); - if (!bcoin.hd.isHD(master) && !MasterKey.isMasterKey(master)) - master = bcoin.hd.from(master, this.network); + if (!HD.isHD(master)) + master = HD.from(master, this.network); - if (bcoin.hd.isHD(master)) master = MasterKey.fromKey(master); - - assert(MasterKey.isMasterKey(master)); + } this.master = master; - if (options.initialized != null) { - assert(typeof options.initialized === 'boolean'); - this.initialized = options.initialized; - } - - if (options.accountDepth != null) { - assert(utils.isNumber(options.accountDepth)); - this.accountDepth = options.accountDepth; - } - if (options.wid != null) { assert(utils.isNumber(options.wid)); this.wid = options.wid; @@ -136,8 +124,20 @@ Wallet.prototype.fromOptions = function fromOptions(options) { id = options.id; } - if (!id) - id = this.getID(); + if (options.initialized != null) { + assert(typeof options.initialized === 'boolean'); + this.initialized = options.initialized; + } + + if (options.watchOnly != null) { + assert(typeof options.watchOnly === 'boolean'); + this.watchOnly = options.watchOnly; + } + + if (options.accountDepth != null) { + assert(utils.isNumber(options.accountDepth)); + this.accountDepth = options.accountDepth; + } if (options.token) { assert(Buffer.isBuffer(options.token)); @@ -150,6 +150,9 @@ Wallet.prototype.fromOptions = function fromOptions(options) { this.tokenDepth = options.tokenDepth; } + if (!id) + id = this.getID(); + if (!token) token = this.getToken(this.master.key, this.tokenDepth); @@ -175,251 +178,414 @@ Wallet.fromOptions = function fromOptions(db, options) { * the first addresses along with the lookahead * addresses). Called automatically from the * walletdb. - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.init = function init(options, callback) { - var self = this; +Wallet.prototype.init = co(function* init(options) { + var account; assert(!this.initialized); this.initialized = true; - this.master.encrypt(options.passphrase, function(err) { - if (err) - return callback(err); + if (options.passphrase) + yield this.master.encrypt(options.passphrase); - self.createAccount(options, function(err, account) { - if (err) - return callback(err); + account = yield this._createAccount(options); + assert(account); - assert(account); + this.account = account; - self.account = account; + this.logger.info('Wallet initialized (%s).', this.id); - self.logger.info('Wallet initialized (%s).', self.id); - - self.tx.open(callback); - }); - }); -}; + yield this.txdb.open(); +}); /** * Open wallet (done after retrieval). - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.open = function open(callback) { - var self = this; +Wallet.prototype.open = co(function* open() { + var account; assert(this.initialized); - this.getAccount(0, function(err, account) { - if (err) - return callback(err); + account = yield this.getAccount(0); - if (!account) - return callback(new Error('Default account not found.')); + if (!account) + throw new Error('Default account not found.'); - self.account = account; + this.account = account; - self.logger.info('Wallet opened (%s).', self.id); + this.logger.info('Wallet opened (%s).', this.id); - self.tx.open(callback); - }); -}; + yield this.txdb.open(); +}); /** * Close the wallet, unregister with the database. - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.destroy = function destroy(callback) { - callback = utils.ensure(callback); - +Wallet.prototype.destroy = co(function* destroy() { + var unlock1 = yield this.writeLock.lock(); + var unlock2 = yield this.fundLock.lock(); try { this.db.unregister(this); this.master.destroy(); - } catch (e) { - this.emit('error', e); - return callback(e); + } finally { + unlock2(); + unlock1(); } - - return utils.nextTick(callback); -}; +}); /** * Add a public account key to the wallet (multisig). * Saves the key in the wallet database. + * @param {(Number|String)} acct * @param {HDPublicKey} key - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.addKey = function addKey(account, key, callback) { - var self = this; +Wallet.prototype.addKey = co(function* addKey(acct, key) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._addKey(acct, key); + } finally { + unlock(); + } +}); - if (typeof key === 'function') { - callback = key; - key = account; - account = null; +/** + * Add a public account key to the wallet without a lock. + * @private + * @param {(Number|String)} acct + * @param {HDPublicKey} key + * @returns {Promise} + */ + +Wallet.prototype._addKey = co(function* addKey(acct, key) { + var account, result; + + if (!key) { + key = acct; + acct = null; } - if (account == null) - account = 0; + if (acct == null) + acct = 0; - callback = this._lockWrite(addKey, [account, key, callback]); + account = yield this.getAccount(acct); - if (!callback) - return; + if (!account) + throw new Error('Account not found.'); - this.getAccount(account, function(err, account) { - if (err) - return callback(err); + this.start(); - if (!account) - return callback(new Error('Account not found.')); + try { + result = yield account.addKey(key); + } catch (e) { + this.drop(); + throw e; + } - self.start(); + yield this.commit(); - account.addKey(key, function(err, result) { - if (err) { - self.drop(); - return callback(err); - } - self.commit(function(err) { - if (err) - return callback(err); - callback(null, result); - }); - }); - }, true); -}; + return result; +}); /** * Remove a public account key from the wallet (multisig). - * Remove the key from the wallet database. + * @param {(Number|String)} acct * @param {HDPublicKey} key - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.removeKey = function removeKey(account, key, callback) { - var self = this; +Wallet.prototype.removeKey = co(function* removeKey(acct, key) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._removeKey(acct, key); + } finally { + unlock(); + } +}); - if (typeof key === 'function') { - callback = key; - key = account; - account = null; +/** + * Remove a public account key from the wallet (multisig). + * @private + * @param {(Number|String)} acct + * @param {HDPublicKey} key + * @returns {Promise} + */ + +Wallet.prototype._removeKey = co(function* removeKey(acct, key) { + var account, result; + + if (!key) { + key = acct; + acct = null; } - if (account == null) - account = 0; + if (acct == null) + acct = 0; - callback = this._lockWrite(removeKey, [account, key, callback]); + account = yield this.getAccount(acct); - if (!callback) - return; + if (!account) + throw new Error('Account not found.'); - this.getAccount(account, function(err, account) { - if (err) - return callback(err); + this.start(); - if (!account) - return callback(new Error('Account not found.')); + try { + result = yield account.removeKey(key); + } catch (e) { + this.drop(); + throw e; + } - self.start(); + yield this.commit(); - account.removeKey(key, function(err, result) { - if (err) { - self.drop(); - return callback(err); - } - self.commit(function(err) { - if (err) - return callback(err); - callback(null, result); - }); - }); - }, true); -}; + return result; +}); /** * Change or set master key's passphrase. * @param {(String|Buffer)?} old * @param {String|Buffer} new_ - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.setPassphrase = function setPassphrase(old, new_, callback) { - var self = this; - - if (typeof new_ === 'function') { - callback = new_; +Wallet.prototype.setPassphrase = co(function* setPassphrase(old, new_) { + if (new_ == null) { new_ = old; old = null; } - callback = this._lockWrite(setPassphrase, [old, new_, callback]); + if (old != null) + yield this.decrypt(old); - if (!callback) - return; + if (new_ != null) + yield this.encrypt(new_); +}); - this.master.decrypt(old, function(err) { - if (err) - return callback(err); +/** + * Encrypt the wallet permanently. + * @param {String|Buffer} passphrase + * @returns {Promise} + */ - self.master.encrypt(new_, function(err) { - if (err) - return callback(err); +Wallet.prototype.encrypt = co(function* encrypt(passphrase) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._encrypt(passphrase); + } finally { + unlock(); + } +}); - self.start(); - self.save(); - self.commit(callback); - }); - }); -}; +/** + * Encrypt the wallet permanently, without a lock. + * @private + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + +Wallet.prototype._encrypt = co(function* encrypt(passphrase) { + var key; + + if (this.master.encrypted) + throw new Error('Wallet is already encrypted.'); + + this.start(); + + try { + key = yield this.master.encrypt(passphrase); + yield this.db.encryptKeys(this, key); + } catch (e) { + this.drop(); + throw e; + } + + key.fill(0); + + this.save(); + + yield this.commit(); +}); + +/** + * Decrypt the wallet permanently. + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + +Wallet.prototype.decrypt = co(function* decrypt(passphrase) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._decrypt(passphrase); + } finally { + unlock(); + } +}); + +/** + * Decrypt the wallet permanently, without a lock. + * @private + * @param {String|Buffer} passphrase + * @returns {Promise} + */ + +Wallet.prototype._decrypt = co(function* decrypt(passphrase) { + var key; + + if (!this.master.encrypted) + throw new Error('Wallet is not encrypted.'); + + this.start(); + + try { + key = yield this.master.decrypt(passphrase); + yield this.db.decryptKeys(this, key); + } catch (e) { + this.drop(); + throw e; + } + + key.fill(0); + + this.save(); + + yield this.commit(); +}); /** * Generate a new token. * @param {(String|Buffer)?} passphrase - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.retoken = function retoken(passphrase, callback) { - var self = this; - - if (typeof passphrase === 'function') { - callback = passphrase; - passphrase = null; +Wallet.prototype.retoken = co(function* retoken(passphrase) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._retoken(passphrase); + } finally { + unlock(); } +}); - callback = this._lockWrite(retoken, [passphrase, callback]); +/** + * Generate a new token without a lock. + * @private + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ - if (!callback) - return; +Wallet.prototype._retoken = co(function* retoken(passphrase) { + var master = yield this.unlock(passphrase); - this.unlock(passphrase, null, function(err, master) { - if (err) - return callback(err); + this.tokenDepth++; + this.token = this.getToken(master, this.tokenDepth); - self.tokenDepth++; - self.token = self.getToken(master, self.tokenDepth); + this.start(); + this.save(); - self.start(); - self.save(); - self.commit(function(err) { - if (err) - return callback(err); - callback(null, self.token); - }); - }); -}; + yield this.commit(); + + return this.token; +}); + +/** + * Rename the wallet. + * @param {String} id + * @returns {Promise} + */ + +Wallet.prototype.rename = co(function* rename(id) { + var unlock = yield this.writeLock.lock(); + try { + return yield this.db.rename(this, id); + } finally { + unlock(); + } +}); + +/** + * Rename account. + * @param {(String|Number)?} acct + * @param {String} name + * @returns {Promise} + */ + +Wallet.prototype.renameAccount = co(function* renameAccount(acct, name) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._renameAccount(acct, name); + } finally { + unlock(); + } +}); + +/** + * Rename account without a lock. + * @private + * @param {(String|Number)?} acct + * @param {String} name + * @returns {Promise} + */ + +Wallet.prototype._renameAccount = co(function* _renameAccount(acct, name) { + var i, account, old, paths, path; + + if (!utils.isName(name)) + throw new Error('Bad account name.'); + + account = yield this.getAccount(acct); + + if (!account) + throw new Error('Account not found.'); + + if (account.accountIndex === 0) + throw new Error('Cannot rename default account.'); + + if (yield this.hasAccount(name)) + throw new Error('Account name not available.'); + + old = account.name; + + this.start(); + + this.db.renameAccount(account, name); + + yield this.commit(); + + this.indexCache.remove(old); + + paths = this.pathCache.values(); + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + + if (path.account !== account.accountIndex) + continue; + + path.name = name; + } +}); /** * Lock the wallet, destroy decrypted key. */ -Wallet.prototype.lock = function lock() { - this.master.destroy(); -}; +Wallet.prototype.lock = co(function* lock() { + var unlock1 = yield this.writeLock.lock(); + var unlock2 = yield this.fundLock.lock(); + try { + this.master.destroy(); + } finally { + unlock2(); + unlock1(); + } +}); /** * Unlock the key for `timeout` milliseconds. @@ -427,8 +593,8 @@ Wallet.prototype.lock = function lock() { * @param {Number?} [timeout=60000] - ms. */ -Wallet.prototype.unlock = function unlock(passphrase, timeout, callback) { - this.master.unlock(passphrase, timeout, callback); +Wallet.prototype.unlock = function unlock(passphrase, timeout) { + return this.master.unlock(passphrase, timeout); }; /** @@ -489,225 +655,345 @@ Wallet.prototype.getToken = function getToken(master, nonce) { /** * Create an account. Requires passphrase if master key is encrypted. * @param {Object} options - See {@link Account} options. - * @param {Function} callback - Returns [Error, {@link Account}]. + * @returns {Promise} - Returns {@link Account}. */ -Wallet.prototype.createAccount = function createAccount(options, callback) { - var self = this; +Wallet.prototype.createAccount = co(function* createAccount(options) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._createAccount(options); + } finally { + unlock(); + } +}); + +/** + * Create an account without a lock. + * @param {Object} options - See {@link Account} options. + * @returns {Promise} - Returns {@link Account}. + */ + +Wallet.prototype._createAccount = co(function* createAccount(options) { var passphrase = options.passphrase; var timeout = options.timeout; var name = options.name; - var key; - - callback = this._lockWrite(createAccount, [options, callback]); - - if (!callback) - return; + var key, master, account, exists; if (typeof options.account === 'string') name = options.account; if (!name) - name = self.accountDepth + ''; + name = this.accountDepth + ''; - this.unlock(passphrase, timeout, function(err, master) { - if (err) - return callback(err); + exists = yield this.hasAccount(name); - key = master.deriveAccount44(self.accountDepth); + if (exists) + throw new Error('Account already exists.'); - options = { - network: self.network, - wid: self.wid, - id: self.id, - name: self.accountDepth === 0 ? 'default' : name, - witness: options.witness, - accountKey: key.hdPublicKey, - accountIndex: self.accountDepth, - type: options.type, - keys: options.keys, - m: options.m, - n: options.n - }; + master = yield this.unlock(passphrase, timeout); - self.start(); + if (this.watchOnly && options.accountKey) { + key = options.accountKey; - self.db.createAccount(options, function(err, account) { - if (err) { - self.drop(); - return callback(err); - } + if (!HD.isHD(key)) + key = HD.from(key, this.network); - self.accountDepth++; - self.save(); - self.commit(function(err) { - if (err) - return callback(err); - callback(null, account); - }); - }); - }); -}; + if (HD.isPrivate(key)) + throw new Error('Cannot add xprivkey to watch-only wallet.'); + } else { + key = master.deriveAccount44(this.accountDepth); + key = key.hdPublicKey; + } + + options = { + wid: this.wid, + id: this.id, + name: this.accountDepth === 0 ? 'default' : name, + witness: options.witness, + watchOnly: this.watchOnly, + accountKey: key, + accountIndex: this.accountDepth, + type: options.type, + m: options.m, + n: options.n, + keys: options.keys + }; + + this.start(); + + try { + account = Account.fromOptions(this.db, options); + } catch (e) { + this.drop(); + throw e; + } + + account.wallet = this; + + yield account.init(); + + this.logger.info('Created account %s/%s/%d.', + account.id, + account.name, + account.accountIndex); + + this.accountDepth++; + this.save(); + + yield this.commit(); + + return account; +}); /** * Ensure an account. Requires passphrase if master key is encrypted. * @param {Object} options - See {@link Account} options. - * @param {Function} callback - Returns [Error, {@link Account}]. + * @returns {Promise} - Returns {@link Account}. */ -Wallet.prototype.ensureAccount = function ensureAccount(options, callback) { - var self = this; - var account = options.account; +Wallet.prototype.ensureAccount = co(function* ensureAccount(options) { + var name = options.account; + var exists; if (typeof options.name === 'string') - account = options.name; + name = options.name; - this.hasAccount(account, function(err, exists) { - if (err) - return callback(err); + exists = yield this.hasAccount(name); - if (exists) - return self.getAccount(account, callback); + if (exists) + return yield this.getAccount(name); - self.createAccount(options, callback); - }); -}; + return this.createAccount(options); +}); /** * List account names and indexes from the db. - * @param {Function} callback - Returns [Error, Array]. + * @returns {Promise} - Returns Array. */ -Wallet.prototype.getAccounts = function getAccounts(callback) { - this.db.getAccounts(this.wid, callback); +Wallet.prototype.getAccounts = function getAccounts() { + return this.db.getAccounts(this.wid); }; /** * Get all wallet address hashes. - * @param {Function} callback - Returns [Error, Array]. + * @returns {Promise} - Returns Array. */ -Wallet.prototype.getAddressHashes = function getAddressHashes(callback) { - this.db.getAddressHashes(this.wid, callback); +Wallet.prototype.getAddressHashes = function getAddressHashes() { + return this.db.getWalletHashes(this.wid); }; /** * Retrieve an account from the database. - * @param {Number|String} account - * @param {Function} callback - Returns [Error, {@link Account}]. + * @param {Number|String} acct + * @returns {Promise} - Returns {@link Account}. */ -Wallet.prototype.getAccount = function getAccount(account, callback) { - var self = this; +Wallet.prototype.getAccount = co(function* getAccount(acct) { + var index, unlock; if (this.account) { - if (account === 0 || account === 'default') - return callback(null, this.account); + if (acct === 0 || acct === 'default') + return this.account; } - this.db.getAccount(this.wid, account, function(err, account) { - if (err) - return callback(err); + index = yield this.getAccountIndex(acct); - if (!account) - return callback(); + if (index === -1) + return; - account.wid = self.wid; - account.id = self.id; + unlock = yield this.readLock.lock(index); - callback(null, account); - }); -}; + try { + return yield this._getAccount(index); + } finally { + unlock(); + } +}); + +/** + * Retrieve an account from the database without a lock. + * @param {Number} index + * @returns {Promise} - Returns {@link Account}. + */ + +Wallet.prototype._getAccount = co(function* getAccount(index) { + var account = this.accountCache.get(index); + + if (account) + return account; + + account = yield this.db.getAccount(this.wid, index); + + if (!account) + return; + + account.wallet = this; + account.wid = this.wid; + account.id = this.id; + account.watchOnly = this.watchOnly; + + yield account.open(); + + this.accountCache.set(index, account); + + return account; +}); + +/** + * Lookup the corresponding account name's index. + * @param {WalletID} wid + * @param {String|Number} name - Account name/index. + * @returns {Promise} - Returns Number. + */ + +Wallet.prototype.getAccountIndex = co(function* getAccountIndex(name) { + var index; + + if (name == null) + return -1; + + if (typeof name === 'number') + return name; + + index = this.indexCache.get(name); + + if (index != null) + return index; + + index = yield this.db.getAccountIndex(this.wid, name); + + if (index === -1) + return -1; + + this.indexCache.set(name, index); + + return index; +}); + +/** + * Lookup the corresponding account index's name. + * @param {WalletID} wid + * @param {Number} index - Account index. + * @returns {Promise} - Returns String. + */ + +Wallet.prototype.getAccountName = co(function* getAccountName(index) { + var account = yield this.getAccount(index); + + if (!account) + return null; + + return account.name; +}); /** * Test whether an account exists. - * @param {Number|String} account - * @param {Function} callback - Returns [Error, {@link Boolean}]. + * @param {Number|String} acct + * @returns {Promise} - Returns {@link Boolean}. */ -Wallet.prototype.hasAccount = function hasAccount(account, callback) { - this.db.hasAccount(this.wid, account, callback); -}; +Wallet.prototype.hasAccount = co(function* hasAccount(acct) { + var index = yield this.getAccountIndex(acct); + + if (index === -1) + return false; + + if (this.accountCache.has(index)) + return true; + + return yield this.db.hasAccount(this.wid, index); +}); /** * Create a new receiving address (increments receiveDepth). - * @param {(Number|String)?} account - * @param {Function} callback - Returns [Error, {@link KeyRing}]. + * @param {(Number|String)?} acct + * @returns {Promise} - Returns {@link WalletKey}. */ -Wallet.prototype.createReceive = function createReceive(account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - return this.createAddress(account, false, callback); +Wallet.prototype.createReceive = function createReceive(acct) { + return this.createKey(acct, 0); }; /** * Create a new change address (increments receiveDepth). - * @param {(Number|String)?} account - * @param {Function} callback - Returns [Error, {@link KeyRing}]. + * @param {(Number|String)?} acct + * @returns {Promise} - Returns {@link WalletKey}. */ -Wallet.prototype.createChange = function createChange(account, callback) { - if (typeof account === 'function') { - callback = account; - account = null; - } - return this.createAddress(account, true, callback); +Wallet.prototype.createChange = function createChange(acct) { + return this.createKey(acct, 1); +}; + +/** + * Create a new nested address (increments receiveDepth). + * @param {(Number|String)?} acct + * @returns {Promise} - Returns {@link WalletKey}. + */ + +Wallet.prototype.createNested = function createNested(acct) { + return this.createKey(acct, 2); }; /** * Create a new address (increments depth). - * @param {(Number|String)?} account - * @param {Boolean} change - * @param {Function} callback - Returns [Error, {@link KeyRing}]. + * @param {(Number|String)?} acct + * @param {Number} branch + * @returns {Promise} - Returns {@link WalletKey}. */ -Wallet.prototype.createAddress = function createAddress(account, change, callback) { - var self = this; +Wallet.prototype.createKey = co(function* createKey(acct, branch) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._createKey(acct, branch); + } finally { + unlock(); + } +}); - if (typeof change === 'function') { - callback = change; - change = account; - account = null; +/** + * Create a new address (increments depth) without a lock. + * @private + * @param {(Number|String)?} acct + * @param {Number} branche + * @returns {Promise} - Returns {@link WalletKey}. + */ + +Wallet.prototype._createKey = co(function* createKey(acct, branch) { + var account, result; + + if (branch == null) { + branch = acct; + acct = null; } - if (account == null) - account = 0; + if (acct == null) + acct = 0; - callback = this._lockWrite(createAddress, [account, change, callback]); + account = yield this.getAccount(acct); - if (!callback) - return; + if (!account) + throw new Error('Account not found.'); - this.getAccount(account, function(err, account) { - if (err) - return callback(err); + this.start(); - if (!account) - return callback(new Error('Account not found.')); + try { + result = yield account.createKey(branch); + } catch (e) { + this.drop(); + throw e; + } - self.start(); + yield this.commit(); - account.createAddress(change, function(err, result) { - if (err) { - self.drop(); - return callback(err); - } - self.commit(function(err) { - if (err) - return callback(err); - callback(null, result); - }); - }); - }, true); -}; + return result; +}); /** * Save the wallet to the database. Necessary * when address depth and keys change. - * @param {Function} callback + * @returns {Promise} */ Wallet.prototype.save = function save() { @@ -720,7 +1006,7 @@ Wallet.prototype.save = function save() { */ Wallet.prototype.start = function start() { - return this.db.start(this.wid); + return this.db.start(this); }; /** @@ -729,164 +1015,260 @@ Wallet.prototype.start = function start() { */ Wallet.prototype.drop = function drop() { - return this.db.drop(this.wid); + return this.db.drop(this); }; /** * Save batch. - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.commit = function commit(callback) { - return this.db.commit(this.wid, callback); +Wallet.prototype.commit = function commit() { + return this.db.commit(this); }; /** * Test whether the wallet possesses an address. * @param {Address|Hash} address - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Wallet.prototype.hasAddress = function hasAddress(address, callback) { - var hash = bcoin.address.getHash(address, 'hex'); +Wallet.prototype.hasAddress = co(function* hasAddress(address) { + var hash = Address.getHash(address, 'hex'); + var path; + if (!hash) - return callback(null, false); - return this.db.hasAddress(this.wid, hash, callback); -}; + return false; + + path = yield this.getPath(hash); + + return path != null; +}); /** * Get path by address hash. * @param {Address|Hash} address - * @param {Function} callback - Returns [Error, {@link Path}]. + * @returns {Promise} - Returns {@link Path}. */ -Wallet.prototype.getPath = function getPath(address, callback) { - var self = this; - var hash = bcoin.address.getHash(address, 'hex'); +Wallet.prototype.getPath = co(function* getPath(address) { + var hash = Address.getHash(address, 'hex'); + var path; if (!hash) - return callback(); + return; - this.db.getAddressPath(this.wid, hash, function(err, path) { - if (err) - return callback(err); + path = this.pathCache.get(hash); - if (!path) - return callback(); + if (path) + return path; - path.id = self.id; + path = yield this.db.getPath(this.wid, hash); - callback(null, path); - }); -}; + if (!path) + return; + + path.id = this.id; + path.name = yield this.getAccountName(path.account); + + assert(path.name); + + // account = yield this.getAccount(path.account); + // assert(account); + // path.name = account.name; + // path.version = account.getWitnessVersion(path); + // path.type = account.getAddressType(path); + + this.pathCache.set(hash, path); + + return path; +}); /** * Get all wallet paths. - * @param {(String|Number)?} account - * @param {Function} callback - Returns [Error, {@link Path}]. + * @param {(String|Number)?} acct + * @returns {Promise} - Returns {@link Path}. */ -Wallet.prototype.getPaths = function getPaths(account, callback) { - var self = this; - var out = []; +Wallet.prototype.getPaths = co(function* getPaths(acct) { + var index = yield this.ensureIndex(acct); + var paths = yield this.db.getWalletPaths(this.wid); + var result = []; var i, path; - this._getIndex(account, callback, function(account, callback) { - this.db.getWalletPaths(this.wid, function(err, paths) { - if (err) - return callback(err); + for (i = 0; i < paths.length; i++) { + path = paths[i]; + if (index == null || path.account === index) { + path.id = this.id; + path.name = yield this.getAccountName(path.account); - for (i = 0; i < paths.length; i++) { - path = paths[i]; - if (!account || path.account === account) { - path.id = self.id; - out.push(path); - } - } + assert(path.name); - callback(null, out); - }); - }); -}; + // account = yield this.getAccount(path.account); + // assert(account); + // path.name = account.name; + // path.version = account.getWitnessVersion(path); + // path.type = account.getAddressType(path); + + this.pathCache.set(path.hash, path); + + result.push(path); + } + } + + return result; +}); /** * Import a keyring (will not exist on derivation chain). * Rescanning must be invoked manually. - * @param {(String|Number)?} account - * @param {KeyRing} ring + * @param {(String|Number)?} acct + * @param {WalletKey} ring * @param {(String|Buffer)?} passphrase - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.importKey = function importKey(account, ring, passphrase, callback) { - var self = this; - var raw, path; +Wallet.prototype.importKey = co(function* importKey(acct, ring, passphrase) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._importKey(acct, ring, passphrase); + } finally { + unlock(); + } +}); - if (typeof passphrase === 'function') { - callback = passphrase; - passphrase = null; +/** + * Import a keyring (will not exist on derivation chain) without a lock. + * @private + * @param {(String|Number)?} acct + * @param {WalletKey} ring + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ + +Wallet.prototype._importKey = co(function* importKey(acct, ring, passphrase) { + var account, exists, path; + + if (acct && typeof acct === 'object') { + passphrase = ring; + ring = acct; + acct = null; } - if (typeof ring === 'function') { - callback = ring; - ring = account; - account = null; + if (acct == null) + acct = 0; + + if (!this.watchOnly) { + if (!ring.privateKey) + throw new Error('Cannot import pubkey into non watch-only wallet.'); + } else { + if (ring.privateKey) + throw new Error('Cannot import privkey into watch-only wallet.'); } - if (account == null) - account = 0; + exists = yield this.getPath(ring.getHash('hex')); - callback = this._lockWrite(importKey, [account, ring, passphrase, callback]); + if (exists) + throw new Error('Key already exists.'); - if (!callback) - return; + account = yield this.getAccount(acct); - this.getPath(ring.getHash('hex'), function(err, exists) { - if (err) - return callback(err); + if (!account) + throw new Error('Account not found.'); - if (exists) - return callback(new Error('Key already exists.')); + if (account.type !== Account.types.PUBKEYHASH) + throw new Error('Cannot import into non-pkh account.'); - self.getAccount(account, function(err, account) { - if (err) - return callback(err); + yield this.unlock(passphrase); - if (!account) - return callback(new Error('Account not found.')); + ring = WalletKey.fromRing(account, ring); + path = ring.toPath(); - if (account.type !== bcoin.account.types.PUBKEYHASH) - return callback(new Error('Cannot import into non-pkh account.')); + if (this.master.encrypted) { + path.data = this.master.encipher(path.data, path.hash); + assert(path.data); + path.encrypted = true; + } - self.unlock(passphrase, null, function(err) { - if (err) - return callback(err); + this.start(); - raw = ring.toRaw(); - path = Path.fromAccount(account, ring); + try { + yield account.savePath(path); + } catch (e) { + this.drop(); + throw e; + } - if (self.master.encrypted) { - raw = self.master.encipher(raw, path.hash); - assert(raw); - path.encrypted = true; - } + yield this.commit(); +}); - path.imported = raw; - ring.path = path; +/** + * Import a keyring (will not exist on derivation chain). + * Rescanning must be invoked manually. + * @param {(String|Number)?} acct + * @param {WalletKey} ring + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ - self.start(); +Wallet.prototype.importAddress = co(function* importAddress(acct, address) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._importAddress(acct, address); + } finally { + unlock(); + } +}); - account.saveAddress([ring], function(err) { - if (err) { - self.drop(); - return callback(err); - } - self.commit(callback); - }); - }, true); - }); - }); -}; +/** + * Import a keyring (will not exist on derivation chain) without a lock. + * @private + * @param {(String|Number)?} acct + * @param {WalletKey} ring + * @param {(String|Buffer)?} passphrase + * @returns {Promise} + */ + +Wallet.prototype._importAddress = co(function* importAddress(acct, address) { + var account, exists, path; + + if (!address) { + address = acct; + acct = null; + } + + if (acct == null) + acct = 0; + + if (!this.watchOnly) + throw new Error('Cannot import address into non watch-only wallet.'); + + exists = yield this.getPath(address); + + if (exists) + throw new Error('Address already exists.'); + + account = yield this.getAccount(acct); + + if (!account) + throw new Error('Account not found.'); + + if (account.type !== Account.types.PUBKEYHASH) + throw new Error('Cannot import into non-pkh account.'); + + path = Path.fromAddress(account, address); + + this.start(); + + try { + yield account.savePath(path); + } catch (e) { + this.drop(); + throw e; + } + + yield this.commit(); +}); /** * Fill a transaction with inputs, estimate @@ -912,82 +1294,76 @@ Wallet.prototype.importKey = function importKey(account, ring, passphrase, callb * fee from existing outputs rather than adding more inputs. */ -Wallet.prototype.fund = function fund(tx, options, callback, force) { - var self = this; - var rate; - - if (typeof options === 'function') { - callback = options; - options = null; +Wallet.prototype.fund = co(function* fund(tx, options, force) { + var unlock = yield this.fundLock.lock(force); + try { + return yield this._fund(tx, options); + } finally { + unlock(); } +}); + +/** + * Fill a transaction with inputs without a lock. + * @private + * @see MTX#selectCoins + * @see MTX#fill + */ + +Wallet.prototype._fund = co(function* fund(tx, options) { + var rate, account, coins; if (!options) options = {}; - // We use a lock here to ensure we - // don't end up double spending coins. - callback = this._lockFund(fund, [tx, options, callback], force); - - if (!callback) - return; - if (!this.initialized) - return callback(new Error('Wallet is not initialized.')); + throw new Error('Wallet is not initialized.'); - this.getAccount(options.account, function(err, account) { - if (err) - return callback(err); + if (this.watchOnly) + throw new Error('Cannot fund from watch-only wallet.'); - if (!account) { - if (options.account != null) - return callback(new Error('Account not found.')); - account = self.account; - } + if (options.account != null) { + account = yield this.getAccount(options.account); + if (!account) + throw new Error('Account not found.'); + } else { + account = this.account; + } - if (!account.initialized) - return callback(new Error('Account is not initialized.')); + if (!account.initialized) + throw new Error('Account is not initialized.'); - self.getCoins(options.account, function(err, coins) { - if (err) - return callback(err); + coins = yield this.getCoins(options.account); - rate = options.rate; + rate = this.network.feeRate; - if (rate == null) { - if (self.db.fees) - rate = self.db.fees.estimateFee(); - else - rate = self.network.getRate(); - } + if (options.rate != null) { + rate = options.rate; + } else { + if (this.db.fees) + rate = this.db.fees.estimateFee(); + } - // Don't use any locked coins. - coins = self.tx.filterLocked(coins); + // Don't use any locked coins. + coins = this.txdb.filterLocked(coins); - try { - tx.fund(coins, { - selection: options.selection, - round: options.round, - confirmations: options.confirmations, - free: options.free, - hardFee: options.hardFee, - subtractFee: options.subtractFee, - changeAddress: account.changeAddress.getAddress(), - height: self.db.height, - rate: rate, - maxFee: options.maxFee, - m: account.m, - n: account.n, - witness: account.witness, - script: account.receiveAddress.script - }); - } catch (e) { - return callback(e); - } - - callback(); - }); + tx.fund(coins, { + selection: options.selection, + round: options.round, + confirmations: options.confirmations, + free: options.free, + hardFee: options.hardFee, + subtractFee: options.subtractFee, + changeAddress: account.change.getAddress(), + height: this.db.height, + rate: rate, + maxFee: options.maxFee, + m: account.m, + n: account.n, + witness: account.witness, + script: account.receive.script }); -}; +}); /** * Build a transaction, fill it with outputs and inputs, @@ -995,62 +1371,49 @@ Wallet.prototype.fund = function fund(tx, options, callback, force) { * and template it. * @param {Object} options - See {@link Wallet#fund options}. * @param {Object[]} options.outputs - See {@link MTX#addOutput}. - * @param {Function} callback - Returns [Error, {@link MTX}]. + * @returns {Promise} - Returns {@link MTX}. */ -Wallet.prototype.createTX = function createTX(options, callback, force) { - var self = this; +Wallet.prototype.createTX = co(function* createTX(options, force) { var outputs = options.outputs; - var i, tx; + var i, tx, total; if (!Array.isArray(outputs) || outputs.length === 0) - return callback(new Error('No outputs.')); + throw new Error('No outputs.'); // Create mutable tx - tx = bcoin.mtx(); + tx = new MTX(); // Add the outputs - for (i = 0; i < outputs.length; i++) { - try { - tx.addOutput(outputs[i]); - } catch (e) { - callback = utils.asyncify(callback); - return callback(e); - } - } + for (i = 0; i < outputs.length; i++) + tx.addOutput(outputs[i]); // Fill the inputs with unspents - this.fund(tx, options, function(err) { - if (err) - return callback(err); + yield this.fund(tx, options, force); - // Sort members a la BIP69 - tx.sortMembers(); + // Sort members a la BIP69 + tx.sortMembers(); - // Set the locktime to target value or - // `height - whatever` to avoid fee sniping. - // if (options.locktime != null) - // tx.setLocktime(options.locktime); - // else - // tx.avoidFeeSniping(self.db.height); + // Set the locktime to target value or + // `height - whatever` to avoid fee sniping. + // if (options.locktime != null) + // tx.setLocktime(options.locktime); + // else + // tx.avoidFeeSniping(this.db.height); - if (!tx.isSane()) - return callback(new Error('CheckTransaction failed.')); + if (!tx.isSane()) + throw new Error('CheckTransaction failed.'); - if (!tx.checkInputs(self.db.height)) - return callback(new Error('CheckInputs failed.')); + if (!tx.checkInputs(this.db.height)) + throw new Error('CheckInputs failed.'); - self.template(tx, function(err, total) { - if (err) - return callback(err); + total = yield this.template(tx); - if (total === 0) - return callback(new Error('template failed.')); + if (total === 0) + throw new Error('template failed.'); - callback(null, tx); - }); - }, force); -}; + return tx; +}); /** * Build a transaction, fill it with outputs and inputs, @@ -1059,205 +1422,167 @@ Wallet.prototype.createTX = function createTX(options, callback, force) { * coins from being double spent. * @param {Object} options - See {@link Wallet#fund options}. * @param {Object[]} options.outputs - See {@link MTX#addOutput}. - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Wallet.prototype.send = function send(options, callback) { - var self = this; +Wallet.prototype.send = co(function* send(options) { + var unlock = yield this.fundLock.lock(); + try { + return yield this._send(options); + } finally { + unlock(); + } +}); - callback = this._lockFund(send, [options, callback]); +/** + * Build and send a transaction without a lock. + * @private + * @param {Object} options - See {@link Wallet#fund options}. + * @param {Object[]} options.outputs - See {@link MTX#addOutput}. + * @returns {Promise} - Returns {@link TX}. + */ - if (!callback) - return; +Wallet.prototype._send = co(function* send(options) { + var tx = yield this.createTX(options, true); - this.createTX(options, function(err, tx) { - if (err) - return callback(err); + yield this.sign(tx, options); - self.sign(tx, options, function(err) { - if (err) - return callback(err); + if (!tx.isSigned()) + throw new Error('TX could not be fully signed.'); - if (!tx.isSigned()) - return callback(new Error('TX could not be fully signed.')); + tx = tx.toTX(); - tx = tx.toTX(); + yield this.db.addTX(tx); - self.addTX(tx, function(err) { - if (err) - return callback(err); + this.logger.debug('Sending wallet tx (%s): %s', this.id, tx.rhash); + this.db.emit('send', tx); - self.logger.debug('Sending wallet tx (%s): %s', self.id, tx.rhash); - self.db.emit('send', tx); - - callback(null, tx); - }); - }); - }, true); -}; + return tx; +}); /** * Resend pending wallet transactions. - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.resend = function resend(callback) { - var self = this; +Wallet.prototype.resend = co(function* resend() { + var txs = yield this.getUnconfirmed(); var i; - this.getUnconfirmed(function(err, txs) { - if (err) - return callback(err); + if (txs.length > 0) + this.logger.info('Rebroadcasting %d transactions.', txs.length); - if (txs.length > 0) - self.logger.info('Rebroadcasting %d transactions.', txs.length); + for (i = 0; i < txs.length; i++) + this.db.emit('send', txs[i]); - for (i = 0; i < txs.length; i++) - self.db.emit('send', txs[i]); - - callback(); - }); -}; + return txs; +}); /** * Derive necessary addresses for signing a transaction. * @param {TX|Input} tx * @param {Number?} index - Input index. - * @param {Function} callback - Returns [Error, {@link KeyRing}[]]. + * @returns {Promise} - Returns {@link WalletKey}[]. */ -Wallet.prototype.deriveInputs = function deriveInputs(tx, callback) { - var self = this; +Wallet.prototype.deriveInputs = co(function* deriveInputs(tx) { var rings = []; - var ring; + var i, paths, path, account, ring; - this.getInputPaths(tx, function(err, paths) { - if (err) - return callback(err); + paths = yield this.getInputPaths(tx); - utils.forEachSerial(paths, function(path, next) { - self.getAccount(path.account, function(err, account) { - if (err) - return next(err); + for (i = 0; i < paths.length; i++) { + path = paths[i]; + account = yield this.getAccount(path.account); - if (!account) - return next(); + if (!account) + continue; - ring = account.derivePath(path, self.master); + ring = account.derivePath(path, this.master); - if (ring) - rings.push(ring); + if (ring) + rings.push(ring); + } - next(); - }); - }, function(err) { - if (err) - return callback(err); - - callback(null, rings); - }); - }); -}; + return rings; +}); /** * Retrieve a single keyring by address. * @param {Address|Hash} hash - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.getKeyRing = function getKeyRing(address, callback) { - var self = this; - var hash = bcoin.address.getHash(address, 'hex'); - var ring; +Wallet.prototype.getKey = co(function* getKey(address) { + var hash = Address.getHash(address, 'hex'); + var path, account; if (!hash) - return callback(); + return; - this.getPath(hash, function(err, path) { - if (err) - return callback(err); + path = yield this.getPath(hash); - if (!path) - return callback(); + if (!path) + return; - self.getAccount(path.account, function(err, account) { - if (err) - return callback(err); + account = yield this.getAccount(path.account); - if (!account) - return callback(); + if (!account) + return; - ring = account.derivePath(path, self.master); - - callback(null, ring); - }); - }); -}; + return account.derivePath(path, this.master); +}); /** * Map input addresses to paths. * @param {TX|Input} tx - * @param {Function} callback - Returns [Error, {@link Path}[]]. + * @returns {Promise} - Returns {@link Path}[]. */ -Wallet.prototype.getInputPaths = function getInputPaths(tx, callback) { - var self = this; +Wallet.prototype.getInputPaths = co(function* getInputPaths(tx) { var paths = []; var hashes = []; - var hash; + var i, hash, path; - function done() { - utils.forEachSerial(hashes, function(hash, next, i) { - self.getPath(hash, function(err, path) { - if (err) - return next(err); - - if (path) - paths.push(path); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - return callback(null, paths); - }); - } - - if (tx instanceof bcoin.input) { + if (tx instanceof Input) { if (!tx.coin) - return callback(new Error('Not all coins available.')); + throw new Error('Not all coins available.'); + hash = tx.coin.getHash('hex'); + if (hash) hashes.push(hash); - return done(); - } - - this.fillCoins(tx, function(err) { - if (err) - return callback(err); + } else { + yield this.fillCoins(tx); if (!tx.hasCoins()) - return callback(new Error('Not all coins available.')); + throw new Error('Not all coins available.'); hashes = tx.getInputHashes('hex'); - done(); - }); -}; + } + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + path = yield this.getPath(hash); + if (path) + paths.push(path); + } + + return paths; +}); /** * Map output addresses to paths. * @param {TX|Output} tx - * @param {Function} callback - Returns [Error, {@link Path}[]]. + * @returns {Promise} - Returns {@link Path}[]. */ -Wallet.prototype.getOutputPaths = function getOutputPaths(tx, callback) { - var self = this; +Wallet.prototype.getOutputPaths = co(function* getOutputPaths(tx) { var paths = []; var hashes = []; - var hash; + var i, hash, path; - if (tx instanceof bcoin.output) { + if (tx instanceof Output) { hash = tx.getHash('hex'); if (hash) hashes.push(hash); @@ -1265,44 +1590,30 @@ Wallet.prototype.getOutputPaths = function getOutputPaths(tx, callback) { hashes = tx.getOutputHashes('hex'); } - utils.forEachSerial(hashes, function(hash, next, i) { - self.getPath(hash, function(err, path) { - if (err) - return next(err); + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + path = yield this.getPath(hash); + if (path) + paths.push(path); + } - if (path) - paths.push(path); - - next(); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, paths); - }); -}; + return paths; +}); /** * Sync address depths based on a transaction's outputs. * This is used for deriving new addresses when * a confirmed transaction is seen. * @param {PathInfo} info - * @param {Function} callback - Returns [Error, Boolean] + * @returns {Promise} - Returns Boolean * (true if new addresses were allocated). */ -Wallet.prototype.syncOutputDepth = function syncOutputDepth(info, callback) { - var self = this; - var receive = []; +Wallet.prototype.syncOutputDepth = co(function* syncOutputDepth(info) { + var derived = []; var accounts = {}; - var i, path; - - callback = this._lockWrite(syncOutputDepth, [info, callback]); - - if (!callback) - return; - - this.start(); + var i, j, path, paths, account; + var receive, change, nested, ring; for (i = 0; i < info.paths.length; i++) { path = info.paths[i]; @@ -1318,106 +1629,76 @@ Wallet.prototype.syncOutputDepth = function syncOutputDepth(info, callback) { accounts = utils.values(accounts); - utils.forEachSerial(accounts, function(paths, next) { - var account = paths[0].account; - var receiveDepth = -1; - var changeDepth = -1; + for (i = 0; i < accounts.length; i++) { + paths = accounts[i]; + account = paths[0].account; + receive = -1; + change = -1; + nested = -1; - for (i = 0; i < paths.length; i++) { - path = paths[i]; + for (j = 0; j < paths.length; j++) { + path = paths[j]; - if (path.change) { - if (path.index > changeDepth) - changeDepth = path.index; - } else { - if (path.index > receiveDepth) - receiveDepth = path.index; + switch (path.branch) { + case 0: + if (path.index > receive) + receive = path.index; + break; + case 1: + if (path.index > change) + change = path.index; + break; + case 2: + if (path.index > nested) + nested = path.index; + break; } } - receiveDepth += 2; - changeDepth += 2; + receive += 2; + change += 2; + nested += 2; - self.getAccount(account, function(err, account) { - if (err) - return next(err); + account = yield this.getAccount(account); - if (!account) - return next(); + if (!account) + continue; - account.setDepth(receiveDepth, changeDepth, function(err, rcv, chng) { - if (err) - return next(err); + ring = yield account.setDepth(receive, change, nested); - if (rcv) - receive.push(rcv); + if (ring) + derived.push(ring); + } - next(); - }); - }); - }, function(err) { - if (err) { - self.drop(); - return callback(err); - } + if (derived.length > 0) { + this.db.emit('address', this.id, derived); + this.emit('address', derived); + } - if (receive.length > 0) { - self.db.emit('address', self.id, receive); - self.emit('address', receive); - } - - self.commit(function(err) { - if (err) - return callback(err); - callback(null, receive); - }); - }); -}; + return derived; +}); /** * Emit balance events after a tx is saved. * @private * @param {TX} tx * @param {PathInfo} info - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.updateBalances = function updateBalances(callback) { - var self = this; +Wallet.prototype.updateBalances = co(function* updateBalances() { + var balance; if (this.db.listeners('balance').length === 0 && this.listeners('balance').length === 0) { - return callback(); + return; } - this.getBalance(function(err, balance) { - if (err) - return callback(err); + balance = yield this.getBalance(); - self.db.emit('balance', self.id, balance); - self.emit('balance', balance); - - callback(); - }); -}; - -/** - * Derive new addresses and emit balance. - * @private - * @param {TX} tx - * @param {PathInfo} info - * @param {Function} callback - */ - -Wallet.prototype.handleTX = function handleTX(info, callback) { - var self = this; - this.syncOutputDepth(info, function(err) { - if (err) - return callback(err); - - self.updateBalances(callback); - }); -}; + this.db.emit('balance', this.id, balance); + this.emit('balance', balance); +}); /** * Get a redeem script or witness script by hash. @@ -1425,322 +1706,363 @@ Wallet.prototype.handleTX = function handleTX(info, callback) { * @returns {Script} */ -Wallet.prototype.getRedeem = function getRedeem(hash, callback) { +Wallet.prototype.getRedeem = co(function* getRedeem(hash) { + var ring; + if (typeof hash === 'string') hash = new Buffer(hash, 'hex'); - this.getKeyRing(hash.toString('hex'), function(err, ring) { - if (err) - return callback(err); + ring = yield this.getKey(hash.toString('hex')); - if (!ring) - return callback(); + if (!ring) + return; - callback(null, ring.getRedeem(hash)); - }); -}; + return ring.getRedeem(hash); +}); /** * Build input scripts templates for a transaction (does not * sign, only creates signature slots). Only builds scripts * for inputs that are redeemable by this wallet. * @param {MTX} tx - * @param {Function} callback - Returns [Error, Number] + * @returns {Promise} - Returns Number * (total number of scripts built). */ -Wallet.prototype.template = function template(tx, callback) { - var total = 0; - var i, ring; - - this.deriveInputs(tx, function(err, rings) { - if (err) - return callback(err); - - for (i = 0; i < rings.length; i++) { - ring = rings[i]; - total += tx.template(ring); - } - - callback(null, total); - }); -}; +Wallet.prototype.template = co(function* template(tx) { + var rings = yield this.deriveInputs(tx); + return tx.template(rings); +}); /** * Build input scripts and sign inputs for a transaction. Only attempts * to build/sign inputs that are redeemable by this wallet. * @param {MTX} tx * @param {Object|String|Buffer} options - Options or passphrase. - * @param {Function} callback - Returns [Error, Number] (total number + * @returns {Promise} - Returns Number (total number * of inputs scripts built and signed). */ -Wallet.prototype.sign = function sign(tx, options, callback) { - var self = this; - var passphrase, timeout; +Wallet.prototype.sign = co(function* sign(tx, options) { + var rings; - if (typeof options === 'function') { - callback = options; + if (!options) options = {}; - } if (typeof options === 'string' || Buffer.isBuffer(options)) options = { passphrase: options }; - passphrase = options.passphrase; - timeout = options.timeout; + if (this.watchOnly) + throw new Error('Cannot sign from a watch-only wallet.'); - this.unlock(passphrase, timeout, function(err, master) { - if (err) - return callback(err); + yield this.unlock(options.passphrase, options.timeout); - self.deriveInputs(tx, function(err, rings) { - if (err) - return callback(err); + rings = yield this.deriveInputs(tx); - self.signAsync(rings, tx, callback); - }); - }); -}; - -/** - * Sign a transaction asynchronously. - * @param {KeyRing[]} rings - * @param {MTX} tx - * @param {Function} callback - Returns [Error, Number] (total number - * of inputs scripts built and signed). - */ - -Wallet.prototype.signAsync = function signAsync(rings, tx, callback) { - var result; - - if (!this.workerPool) { - callback = utils.asyncify(callback); - try { - result = tx.sign(rings); - } catch (e) { - return callback(e); - } - return callback(null, result); - } - - this.workerPool.sign(tx, rings, null, callback); -}; + return yield tx.signAsync(rings); +}); /** * Fill transaction with coins (accesses db). * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Wallet.prototype.fillCoins = function fillCoins(tx, callback) { - return this.tx.fillCoins(tx, callback); +Wallet.prototype.fillCoins = function fillCoins(tx) { + return this.txdb.fillCoins(tx); }; /** * Fill transaction with historical coins (accesses db). * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Wallet.prototype.fillHistory = function fillHistory(tx, callback) { - return this.tx.fillHistory(tx, callback); +Wallet.prototype.fillHistory = function fillHistory(tx) { + return this.txdb.fillHistory(tx); }; /** * Fill transaction with historical coins (accesses db). * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Wallet.prototype.toDetails = function toDetails(tx, callback) { - return this.tx.toDetails(tx, callback); +Wallet.prototype.toDetails = function toDetails(tx) { + return this.txdb.toDetails(tx); }; /** * Fill transaction with historical coins (accesses db). * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Wallet.prototype.getDetails = function getDetails(tx, callback) { - return this.tx.getDetails(tx, callback); +Wallet.prototype.getDetails = function getDetails(tx) { + return this.txdb.getDetails(tx); }; /** * Get a coin from the wallet (accesses db). * @param {Hash} hash * @param {Number} index - * @param {Function} callback - Returns [Error, {@link Coin}]. + * @returns {Promise} - Returns {@link Coin}. */ -Wallet.prototype.getCoin = function getCoin(hash, index, callback) { - return this.tx.getCoin(hash, index, callback); +Wallet.prototype.getCoin = function getCoin(hash, index) { + return this.txdb.getCoin(hash, index); }; /** * Get a transaction from the wallet (accesses db). * @param {Hash} hash - * @param {Function} callback - Returns [Error, {@link TX}]. + * @returns {Promise} - Returns {@link TX}. */ -Wallet.prototype.getTX = function getTX(hash, callback) { - return this.tx.getTX(hash, callback); +Wallet.prototype.getTX = function getTX(hash) { + return this.txdb.getTX(hash); }; /** * Add a transaction to the wallets TX history (accesses db). * @param {TX} tx - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype.addTX = function addTX(tx, callback) { - this.db.addTX(tx, callback); -}; - -/** - * Get all transactions in transaction history (accesses db). - * @param {(String|Number)?} account - * @param {Function} callback - Returns [Error, {@link TX}[]]. - */ - -Wallet.prototype.getHistory = function getHistory(account, callback) { - this._getIndex(account, callback, function(account, callback) { - this.tx.getHistory(account, callback); - }); -}; - -/** - * Get all available coins (accesses db). - * @param {(String|Number)?} account - * @param {Function} callback - Returns [Error, {@link Coin}[]]. - */ - -Wallet.prototype.getCoins = function getCoins(account, callback) { - this._getIndex(account, callback, function(account, callback) { - this.tx.getCoins(account, callback); - }); -}; - -/** - * Get all pending/unconfirmed transactions (accesses db). - * @param {(String|Number)?} account - * @param {Function} callback - Returns [Error, {@link TX}[]]. - */ - -Wallet.prototype.getUnconfirmed = function getUnconfirmed(account, callback) { - this._getIndex(account, callback, function(account, callback) { - this.tx.getUnconfirmed(account, callback); - }); -}; - -/** - * Get wallet balance (accesses db). - * @param {(String|Number)?} account - * @param {Function} callback - Returns [Error, {@link Balance}]. - */ - -Wallet.prototype.getBalance = function getBalance(account, callback) { - this._getIndex(account, callback, function(account, callback) { - this.tx.getBalance(account, callback); - }); -}; - -/** - * Get a range of transactions between two timestamps (accesses db). - * @param {(String|Number)?} account - * @param {Object} options - * @param {Number} options.start - * @param {Number} options.end - * @param {Function} callback - Returns [Error, {@link TX}[]]. - */ - -Wallet.prototype.getRange = function getRange(account, options, callback) { - if (typeof options === 'function') { - callback = options; - options = account; - account = null; +Wallet.prototype.add = co(function* add(tx) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._add(tx); + } finally { + unlock(); } - this._getIndex(account, callback, function(account, callback) { - this.tx.getRange(account, options, callback); - }); -}; +}); /** - * Get the last N transactions (accesses db). - * @param {(String|Number)?} account - * @param {Number} limit - * @param {Function} callback - Returns [Error, {@link TX}[]]. + * Add a transaction to the wallet without a lock. + * @param {TX} tx + * @returns {Promise} */ -Wallet.prototype.getLast = function getLast(account, limit, callback) { - if (typeof limit === 'function') { - callback = limit; - limit = account; - account = null; +Wallet.prototype._add = co(function* add(tx) { + var info = yield this.getPathInfo(tx); + + this.start(); + + try { + yield this.txdb._add(tx, info); + yield this.syncOutputDepth(info); + yield this.updateBalances(); + } catch (e) { + this.drop(); + throw e; } - this._getIndex(account, callback, function(account, callback) { - this.tx.getLast(account, limit, callback); - }); -}; + + yield this.commit(); +}); + +/** + * Unconfirm a wallet transcation. + * @param {Hash} hash + * @returns {Promise} + */ + +Wallet.prototype.unconfirm = co(function* unconfirm(hash) { + var unlock = yield this.writeLock.lock(); + try { + return yield this.txdb.unconfirm(hash); + } finally { + unlock(); + } +}); + +/** + * Remove a wallet transaction. + * @param {Hash} hash + * @returns {Promise} + */ + +Wallet.prototype.remove = co(function* remove(hash) { + var unlock = yield this.writeLock.lock(); + try { + return yield this.txdb.remove(hash); + } finally { + unlock(); + } +}); /** * Zap stale TXs from wallet (accesses db). - * @param {(Number|String)?} account + * @param {(Number|String)?} acct * @param {Number} age - Age threshold (unix time, default=72 hours). - * @param {Function} callback - Returns [Error]. + * @returns {Promise} */ -Wallet.prototype.zap = function zap(account, age, callback) { - if (typeof age === 'function') { - callback = age; - age = account; - account = null; +Wallet.prototype.zap = co(function* zap(acct, age) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._zap(acct, age); + } finally { + unlock(); } - this._getIndex(account, callback, function(account, callback) { - this.tx.zap(account, age, callback); - }); -}; +}); + +/** + * Zap stale TXs from wallet without a lock. + * @private + * @param {(Number|String)?} acct + * @param {Number} age + * @returns {Promise} + */ + +Wallet.prototype._zap = co(function* zap(acct, age) { + var account = yield this.ensureIndex(acct); + return yield this.txdb.zap(account, age); +}); /** * Abandon transaction (accesses db). * @param {Hash} hash - * @param {Function} callback - Returns [Error]. + * @returns {Promise} */ -Wallet.prototype.abandon = function abandon(hash, callback) { - this.tx.abandon(hash, callback); +Wallet.prototype.abandon = co(function* abandon(hash) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._abandon(hash); + } finally { + unlock(); + } +}); + +/** + * Abandon transaction without a lock. + * @private + * @param {Hash} hash + * @returns {Promise} + */ + +Wallet.prototype._abandon = function abandon(hash) { + return this.txdb.abandon(hash); }; +/** + * Map a transactions' addresses to wallet IDs. + * @param {TX} tx + * @returns {Promise} - Returns {@link PathInfo}. + */ + +Wallet.prototype.getPathInfo = co(function* getPathInfo(tx) { + var hashes = tx.getHashes('hex'); + var paths = []; + var i, hash, path; + + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; + path = yield this.getPath(hash); + if (path) + paths.push(path); + } + + return new PathInfo(this, tx, paths); +}); + +/** + * Get all transactions in transaction history (accesses db). + * @param {(String|Number)?} acct + * @returns {Promise} - Returns {@link TX}[]. + */ + +Wallet.prototype.getHistory = co(function* getHistory(acct) { + var account = yield this.ensureIndex(acct); + return this.txdb.getHistory(account); +}); + +/** + * Get all available coins (accesses db). + * @param {(String|Number)?} account + * @returns {Promise} - Returns {@link Coin}[]. + */ + +Wallet.prototype.getCoins = co(function* getCoins(acct) { + var account = yield this.ensureIndex(acct); + return yield this.txdb.getCoins(account); +}); + +/** + * Get all pending/unconfirmed transactions (accesses db). + * @param {(String|Number)?} acct + * @returns {Promise} - Returns {@link TX}[]. + */ + +Wallet.prototype.getUnconfirmed = co(function* getUnconfirmed(acct) { + var account = yield this.ensureIndex(acct); + return yield this.txdb.getUnconfirmed(account); +}); + +/** + * Get wallet balance (accesses db). + * @param {(String|Number)?} acct + * @returns {Promise} - Returns {@link Balance}. + */ + +Wallet.prototype.getBalance = co(function* getBalance(acct) { + var account = yield this.ensureIndex(acct); + return yield this.txdb.getBalance(account); +}); + +/** + * Get a range of transactions between two timestamps (accesses db). + * @param {(String|Number)?} acct + * @param {Object} options + * @param {Number} options.start + * @param {Number} options.end + * @returns {Promise} - Returns {@link TX}[]. + */ + +Wallet.prototype.getRange = co(function* getRange(acct, options) { + var account; + if (acct && typeof acct === 'object') { + options = acct; + acct = null; + } + account = yield this.ensureIndex(acct); + return yield this.txdb.getRange(account, options); +}); + +/** + * Get the last N transactions (accesses db). + * @param {(String|Number)?} acct + * @param {Number} limit + * @returns {Promise} - Returns {@link TX}[]. + */ + +Wallet.prototype.getLast = co(function* getLast(acct, limit) { + var account = yield this.ensureIndex(acct); + return yield this.txdb.getLast(account, limit); +}); + /** * Resolve account index. * @private - * @param {(Number|String)?} account + * @param {(Number|String)?} acct * @param {Function} errback - Returns [Error]. - * @param {Function} callback + * @returns {Promise} */ -Wallet.prototype._getIndex = function _getIndex(account, errback, callback) { - var self = this; +Wallet.prototype.ensureIndex = co(function* ensureIndex(acct) { + var index; - if (typeof account === 'function') { - errback = account; - account = null; - } + if (acct == null) + return null; - if (account == null) - return callback.call(this, null, errback); + index = yield this.getAccountIndex(acct); - this.db.getAccountIndex(this.wid, account, function(err, index) { - if (err) - return errback(err); + if (index === -1) + throw new Error('Account not found.'); - if (index === -1) - return errback(new Error('Account not found.')); - - callback.call(self, index, errback); - }); -}; + return index; +}); /** * Get public key for current receiving address. @@ -1749,9 +2071,9 @@ Wallet.prototype._getIndex = function _getIndex(account, errback, callback) { */ Wallet.prototype.getPublicKey = function getPublicKey(enc) { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getPublicKey(enc); + return this.receive.getPublicKey(enc); }; /** @@ -1760,9 +2082,9 @@ Wallet.prototype.getPublicKey = function getPublicKey(enc) { */ Wallet.prototype.getScript = function getScript() { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getScript(); + return this.receive.getScript(); }; /** @@ -1772,9 +2094,9 @@ Wallet.prototype.getScript = function getScript() { */ Wallet.prototype.getScriptHash = function getScriptHash(enc) { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getScriptHash(enc); + return this.receive.getScriptHash(enc); }; /** @@ -1784,9 +2106,9 @@ Wallet.prototype.getScriptHash = function getScriptHash(enc) { */ Wallet.prototype.getScriptHash160 = function getScriptHash160(enc) { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getScriptHash160(enc); + return this.receive.getScriptHash160(enc); }; /** @@ -1796,9 +2118,9 @@ Wallet.prototype.getScriptHash160 = function getScriptHash160(enc) { */ Wallet.prototype.getScriptHash256 = function getScriptHash256(enc) { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getScriptHash256(enc); + return this.receive.getScriptHash256(enc); }; /** @@ -1808,9 +2130,9 @@ Wallet.prototype.getScriptHash256 = function getScriptHash256(enc) { */ Wallet.prototype.getScriptAddress = function getScriptAddress(enc) { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getScriptAddress(enc); + return this.receive.getScriptAddress(enc); }; /** @@ -1819,9 +2141,9 @@ Wallet.prototype.getScriptAddress = function getScriptAddress(enc) { */ Wallet.prototype.getProgram = function getProgram() { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getProgram(); + return this.receive.getProgram(); }; /** @@ -1831,10 +2153,10 @@ Wallet.prototype.getProgram = function getProgram() { * @returns {Buffer} */ -Wallet.prototype.getProgramHash = function getProgramHash(enc) { - if (!this.receiveAddress) +Wallet.prototype.getNestedHash = function getNestedHash(enc) { + if (!this.nested) return; - return this.receiveAddress.getProgramHash(enc); + return this.nested.getHash(enc); }; /** @@ -1844,10 +2166,10 @@ Wallet.prototype.getProgramHash = function getProgramHash(enc) { * @returns {Address|Base58Address} */ -Wallet.prototype.getProgramAddress = function getProgramAddress(enc) { - if (!this.receiveAddress) +Wallet.prototype.getNestedAddress = function getNestedAddress(enc) { + if (!this.nested) return; - return this.receiveAddress.getProgramAddress(enc); + return this.nested.getAddress(enc); }; /** @@ -1857,9 +2179,9 @@ Wallet.prototype.getProgramAddress = function getProgramAddress(enc) { */ Wallet.prototype.getKeyHash = function getKeyHash(enc) { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getKeyHash(enc); + return this.receive.getKeyHash(enc); }; /** @@ -1869,9 +2191,9 @@ Wallet.prototype.getKeyHash = function getKeyHash(enc) { */ Wallet.prototype.getKeyAddress = function getKeyAddress(enc) { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getKeyAddress(enc); + return this.receive.getKeyAddress(enc); }; /** @@ -1881,9 +2203,9 @@ Wallet.prototype.getKeyAddress = function getKeyAddress(enc) { */ Wallet.prototype.getHash = function getHash(enc) { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getHash(enc); + return this.receive.getHash(enc); }; /** @@ -1893,9 +2215,9 @@ Wallet.prototype.getHash = function getHash(enc) { */ Wallet.prototype.getAddress = function getAddress(enc) { - if (!this.receiveAddress) + if (!this.receive) return; - return this.receiveAddress.getAddress(enc); + return this.receive.getAddress(enc); }; Wallet.prototype.__defineGetter__('publicKey', function() { @@ -1926,12 +2248,12 @@ Wallet.prototype.__defineGetter__('program', function() { return this.getProgram(); }); -Wallet.prototype.__defineGetter__('programHash', function() { - return this.getProgramHash(); +Wallet.prototype.__defineGetter__('nestedHash', function() { + return this.getNestedHash(); }); -Wallet.prototype.__defineGetter__('programAddress', function() { - return this.getProgramAddress(); +Wallet.prototype.__defineGetter__('nestedAddress', function() { + return this.getNestedAddress(); }); Wallet.prototype.__defineGetter__('keyHash', function() { @@ -1962,22 +2284,34 @@ Wallet.prototype.__defineGetter__('changeDepth', function() { return this.account.changeDepth; }); +Wallet.prototype.__defineGetter__('nestedDepth', function() { + if (!this.account) + return -1; + return this.account.nestedDepth; +}); + Wallet.prototype.__defineGetter__('accountKey', function() { if (!this.account) return; return this.account.accountKey; }); -Wallet.prototype.__defineGetter__('receiveAddress', function() { +Wallet.prototype.__defineGetter__('receive', function() { if (!this.account) return; - return this.account.receiveAddress; + return this.account.receive; }); -Wallet.prototype.__defineGetter__('changeAddress', function() { +Wallet.prototype.__defineGetter__('change', function() { if (!this.account) return; - return this.account.changeAddress; + return this.account.change; +}); + +Wallet.prototype.__defineGetter__('nested', function() { + if (!this.account) + return; + return this.account.nested; }); /** @@ -2012,6 +2346,7 @@ Wallet.prototype.toJSON = function toJSON() { wid: this.wid, id: this.id, initialized: this.initialized, + watchOnly: this.watchOnly, accountDepth: this.accountDepth, token: this.token.toString('hex'), tokenDepth: this.tokenDepth, @@ -2027,22 +2362,29 @@ Wallet.prototype.toJSON = function toJSON() { */ Wallet.prototype.fromJSON = function fromJSON(json) { + var network; + assert(utils.isNumber(json.wid)); assert(typeof json.initialized === 'boolean'); + assert(typeof json.watchOnly === 'boolean'); assert(utils.isName(json.id), 'Bad wallet ID.'); assert(utils.isNumber(json.accountDepth)); assert(typeof json.token === 'string'); assert(json.token.length === 64); assert(utils.isNumber(json.tokenDepth)); - this.network = bcoin.network.get(json.network); + network = Network.get(json.network); + this.wid = json.wid; this.id = json.id; this.initialized = json.initialized; + this.watchOnly = json.watchOnly; this.accountDepth = json.accountDepth; this.token = new Buffer(json.token, 'hex'); this.master = MasterKey.fromJSON(json.master); + assert(network === this.db.network, 'Wallet network mismatch.'); + return this; }; @@ -2056,8 +2398,9 @@ Wallet.prototype.toRaw = function toRaw(writer) { p.writeU32(this.network.magic); p.writeU32(this.wid); - p.writeVarString(this.id, 'utf8'); + p.writeVarString(this.id, 'ascii'); p.writeU8(this.initialized ? 1 : 0); + p.writeU8(this.watchOnly ? 1 : 0); p.writeU32(this.accountDepth); p.writeBytes(this.token); p.writeU32(this.tokenDepth); @@ -2077,14 +2420,21 @@ Wallet.prototype.toRaw = function toRaw(writer) { Wallet.prototype.fromRaw = function fromRaw(data) { var p = new BufferReader(data); - this.network = bcoin.network.fromMagic(p.readU32()); + var network; + + network = Network.fromMagic(p.readU32()); + this.wid = p.readU32(); - this.id = p.readVarString('utf8'); + this.id = p.readVarString('ascii'); this.initialized = p.readU8() === 1; + this.watchOnly = p.readU8() === 1; this.accountDepth = p.readU32(); this.token = p.readBytes(32); this.tokenDepth = p.readU32(); this.master = MasterKey.fromRaw(p.readVarBytes()); + + assert(network === this.db.network, 'Wallet network mismatch.'); + return this; }; @@ -2121,470 +2471,6 @@ Wallet.isWallet = function isWallet(obj) { && obj.template === 'function'; }; -/** - * Master BIP32 key which can exist - * in a timed out encrypted state. - * @exports Master - * @constructor - * @param {Object} options - */ - -function MasterKey(options) { - if (!(this instanceof MasterKey)) - return new MasterKey(options); - - this.encrypted = false; - this.iv = null; - this.ciphertext = null; - this.key = null; - - this.aesKey = null; - this.timer = null; - this.until = 0; - this._destroy = this.destroy.bind(this); - this.locker = new bcoin.locker(this); -} - -/** - * Inject properties from options object. - * @private - * @param {Object} options - */ - -MasterKey.prototype.fromOptions = function fromOptions(options) { - assert(options); - - if (options.encrypted != null) { - assert(typeof options.encrypted === 'boolean'); - this.encrypted = options.encrypted; - } - - if (options.iv) { - assert(Buffer.isBuffer(options.iv)); - this.iv = options.iv; - } - - if (options.ciphertext) { - assert(Buffer.isBuffer(options.ciphertext)); - this.ciphertext = options.ciphertext; - } - - if (options.key) { - assert(bcoin.hd.isHD(options.key)); - this.key = options.key; - } - - assert(this.encrypted ? !this.key : this.key); - - return this; -}; - -/** - * Instantiate master key from options. - * @returns {MasterKey} - */ - -MasterKey.fromOptions = function fromOptions(options) { - return new MasterKey().fromOptions(options); -}; - -/** - * Invoke mutex lock. - * @private - */ - -MasterKey.prototype._lock = function _lock(func, args, force) { - return this.locker.lock(func, args, force); -}; - -/** - * Decrypt the key and set a timeout to destroy decrypted data. - * @param {Buffer|String} passphrase - Zero this yourself. - * @param {Number} [timeout=60000] timeout in ms. - * @param {Function} callback - Returns [Error, {@link HDPrivateKey}]. - */ - -MasterKey.prototype.unlock = function unlock(passphrase, timeout, callback) { - var self = this; - - callback = this._lock(unlock, [passphrase, timeout, callback]); - - if (!callback) - return; - - if (this.key) - return callback(null, this.key); - - if (!passphrase) - return callback(new Error('No passphrase.')); - - assert(this.encrypted); - - crypto.decrypt(this.ciphertext, passphrase, this.iv, function(err, data, key) { - if (err) - return callback(err); - - try { - self.key = bcoin.hd.fromExtended(data); - } catch (e) { - return callback(e); - } - - self.start(timeout); - - self.aesKey = key; - - callback(null, self.key); - }); -}; - -/** - * Start the destroy timer. - * @private - * @param {Number} [timeout=60000] timeout in ms. - */ - -MasterKey.prototype.start = function start(timeout) { - if (!timeout) - timeout = 60000; - - this.stop(); - - if (timeout === -1) - return; - - this.until = utils.now() + (timeout / 1000 | 0); - this.timer = setTimeout(this._destroy, timeout); -}; - -/** - * Stop the destroy timer. - * @private - */ - -MasterKey.prototype.stop = function stop() { - if (this.timer != null) { - clearTimeout(this.timer); - this.timer = null; - this.until = 0; - } -}; - -MasterKey.prototype.encipher = function encipher(data, iv) { - if (!this.aesKey) - return; - - if (typeof iv === 'string') - iv = new Buffer(iv, 'hex'); - - return crypto.encipher(data, this.aesKey, iv.slice(0, 16)); -}; - -MasterKey.prototype.decipher = function decipher(data, iv) { - if (!this.aesKey) - return; - - if (typeof iv === 'string') - iv = new Buffer(iv, 'hex'); - - return crypto.decipher(data, this.aesKey, iv.slice(0, 16)); -}; - -/** - * Destroy the key by zeroing the - * privateKey and chainCode. Stop - * the timer if there is one. - */ - -MasterKey.prototype.destroy = function destroy() { - if (!this.encrypted) { - assert(this.timer == null); - assert(this.key); - return; - } - - this.stop(); - - if (this.key) { - this.key.destroy(true); - this.key = null; - } - - if (this.aesKey) { - this.aesKey.fill(0); - this.aesKey = null; - } -}; - -/** - * Decrypt the key permanently. - * @param {Buffer|String} passphrase - Zero this yourself. - * @param {Function} callback - */ - -MasterKey.prototype.decrypt = function decrypt(passphrase, callback) { - var self = this; - - callback = this._lock(decrypt, [passphrase, callback]); - - if (!callback) - return; - - if (!this.encrypted) { - assert(this.key); - return callback(); - } - - if (!passphrase) - return callback(); - - this.destroy(); - - crypto.decrypt(this.ciphertext, passphrase, this.iv, function(err, data) { - if (err) - return callback(err); - - try { - self.key = bcoin.hd.fromExtended(data); - } catch (e) { - return callback(e); - } - - self.encrypted = false; - self.iv = null; - self.ciphertext = null; - - callback(); - }); -}; - -/** - * Encrypt the key permanently. - * @param {Buffer|String} passphrase - Zero this yourself. - * @param {Function} callback - */ - -MasterKey.prototype.encrypt = function encrypt(passphrase, callback) { - var self = this; - var data, iv; - - callback = this._lock(encrypt, [passphrase, callback]); - - if (!callback) - return; - - if (this.encrypted) - return; - - if (!passphrase) - return callback(); - - data = this.key.toExtended(); - iv = crypto.randomBytes(16); - - this.stop(); - - crypto.encrypt(data, passphrase, iv, function(err, data) { - if (err) - return callback(err); - - self.key = null; - self.encrypted = true; - self.iv = iv; - self.ciphertext = data; - - callback(); - }); -}; - -/** - * Serialize the key in the form of: - * `[enc-flag][iv?][ciphertext?][extended-key?]` - * @returns {Buffer} - */ - -MasterKey.prototype.toRaw = function toRaw(writer) { - var p = new BufferWriter(writer); - - if (this.encrypted) { - p.writeU8(1); - 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(); - - return p; - } - - p.writeU8(0); - p.writeVarBytes(this.key.toExtended()); - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Inject properties from serialized data. - * @private - * @param {Buffer} raw - */ - -MasterKey.prototype.fromRaw = function fromRaw(raw) { - var p = new BufferReader(raw); - - this.encrypted = p.readU8() === 1; - - if (this.encrypted) { - this.iv = p.readVarBytes(); - this.ciphertext = p.readVarBytes(); - - // Future-proofing: - assert(p.readU8() === 0); - assert(p.readU32() === 50000); - assert(p.readU32() === 0); - assert(p.readU32() === 0); - - return this; - } - - this.key = bcoin.hd.fromExtended(p.readVarBytes()); - - return this; -}; - -/** - * Instantiate master key from serialized data. - * @returns {MasterKey} - */ - -MasterKey.fromRaw = function fromRaw(raw) { - return new MasterKey().fromRaw(raw); -}; - -/** - * Inject properties from an HDPrivateKey. - * @private - * @param {HDPrivateKey} key - */ - -MasterKey.prototype.fromKey = function fromKey(key) { - this.encrypted = false; - this.iv = null; - this.ciphertext = null; - this.key = key; - return this; -}; - -/** - * Instantiate master key from an HDPrivateKey. - * @param {HDPrivateKey} key - * @returns {MasterKey} - */ - -MasterKey.fromKey = function fromKey(key) { - return new MasterKey().fromKey(key); -}; - -/** - * Convert master key to a jsonifiable object. - * @returns {Object} - */ - -MasterKey.prototype.toJSON = function toJSON() { - if (this.encrypted) { - return { - encrypted: true, - iv: this.iv.toString('hex'), - ciphertext: this.ciphertext.toString('hex'), - // Future-proofing: - algorithm: 'pbkdf2', - N: 50000, - r: 0, - p: 0 - }; - } - - return { - encrypted: false, - key: this.key.toJSON() - }; -}; - -/** - * Inject properties from JSON object. - * @private - * @param {Object} json - */ - -MasterKey.prototype.fromJSON = function fromJSON(json) { - assert(typeof json.encrypted === 'boolean'); - - this.encrypted = json.encrypted; - - if (json.encrypted) { - assert(typeof json.iv === 'string'); - assert(typeof json.ciphertext === 'string'); - // Future-proofing: - assert(json.algorithm === 'pbkdf2'); - assert(json.N === 50000); - assert(json.r === 0); - assert(json.p === 0); - this.iv = new Buffer(json.iv, 'hex'); - this.ciphertext = new Buffer(json.ciphertext, 'hex'); - } else { - this.key = bcoin.hd.fromJSON(json.key); - } - - return this; -}; - -/** - * Instantiate master key from jsonified object. - * @param {Object} json - * @returns {MasterKey} - */ - -MasterKey.fromJSON = function fromJSON(json) { - return new MasterKey().fromJSON(json); -}; - -/** - * Inspect the key. - * @returns {Object} - */ - -MasterKey.prototype.inspect = function inspect() { - var json = this.toJSON(); - if (this.key) - json.key = this.key.toJSON(); - return json; -}; - -/** - * Test whether an object is a MasterKey. - * @param {Object} obj - * @returns {Boolean} - */ - -MasterKey.isMasterKey = function isMasterKey(obj) { - return obj - && typeof obj.encrypted === 'boolean' - && typeof obj.decrypt === 'function'; -}; - /* * Expose */ diff --git a/lib/wallet/walletdb.js b/lib/wallet/walletdb.js index add70826..8aa2fcd5 100644 --- a/lib/wallet/walletdb.js +++ b/lib/wallet/walletdb.js @@ -7,20 +7,29 @@ 'use strict'; -var bcoin = require('../env'); var AsyncObject = require('../utils/async'); var utils = require('../utils/utils'); +var co = require('../utils/co'); +var Locker = require('../utils/locker'); +var LRU = require('../utils/lru'); var crypto = require('../crypto/crypto'); -var assert = utils.assert; -var constants = bcoin.constants; +var assert = require('assert'); +var constants = require('../protocol/constants'); +var Network = require('../protocol/network'); var BufferReader = require('../utils/reader'); var BufferWriter = require('../utils/writer'); var Path = require('./path'); -var MAX_POINT = String.fromCharCode(0xdbff, 0xdfff); // U+10FFFF +var Wallet = require('./wallet'); +var Account = require('./account'); +var ldb = require('../db/ldb'); +var Bloom = require('../utils/bloom'); +var Logger = require('../node/logger'); +var TX = require('../primitives/tx'); /* * Database Layout: - * p[addr-hash] -> path data + * p[addr-hash] -> wallet ids + * P[wid][addr-hash] -> path data * w[wid] -> wallet * l[id] -> wid * a[wid][index] -> account @@ -41,6 +50,16 @@ var layout = { pp: function(key) { return key.toString('hex', 1); }, + P: function(wid, hash) { + var key = new Buffer(1 + 4 + (hash.length / 2)); + key[0] = 0x50; + key.writeUInt32BE(wid, 1, true); + key.write(hash, 5, 'hex'); + return key; + }, + Pp: function(key) { + return key.toString('hex', 5); + }, w: function(wid) { var key = new Buffer(5); key[0] = 0x77; @@ -51,15 +70,15 @@ var layout = { return key.readUInt32BE(1, true); }, l: function(id) { - var len = Buffer.byteLength(id, 'utf8'); + var len = Buffer.byteLength(id, 'ascii'); var key = new Buffer(1 + len); key[0] = 0x6c; if (len > 0) - key.write(id, 1, 'utf8'); + key.write(id, 1, 'ascii'); return key; }, ll: function(key) { - return key.toString('utf8', 1); + return key.toString('ascii', 1); }, a: function a(wid, index) { var key = new Buffer(9); @@ -69,16 +88,16 @@ var layout = { return key; }, i: function i(wid, name) { - var len = Buffer.byteLength(name, 'utf8'); + var len = Buffer.byteLength(name, 'ascii'); var key = new Buffer(5 + len); key[0] = 0x69; key.writeUInt32BE(wid, 1, true); if (len > 0) - key.write(name, 5, 'utf8'); + key.write(name, 5, 'ascii'); return key; }, ii: function ii(key) { - return [key.readUInt32BE(1, true), key.toString('utf8', 5)]; + return [key.readUInt32BE(1, true), key.toString('ascii', 5)]; }, R: new Buffer([0x52]), b: function b(hash) { @@ -121,26 +140,23 @@ function WalletDB(options) { AsyncObject.call(this); this.options = options; - this.network = bcoin.network.get(options.network); + this.network = Network.get(options.network); this.fees = options.fees; - this.logger = options.logger || bcoin.defaultLogger; - this.batches = {}; - this.wallets = {}; - this.workerPool = null; + this.logger = options.logger || Logger.global; this.tip = this.network.genesis.hash; this.height = 0; this.depth = 0; + this.wallets = {}; // We need one read lock for `get` and `create`. // It will hold locks specific to wallet ids. - this.readLock = new bcoin.locker.mapped(this); - this.writeLock = new bcoin.locker.mapped(this); - this.txLock = new bcoin.locker(this); + this.readLock = new Locker.Mapped(); + this.writeLock = new Locker(); + this.txLock = new Locker(); - this.walletCache = new bcoin.lru(10000); - this.accountCache = new bcoin.lru(10000); - this.pathCache = new bcoin.lru(100000); + this.widCache = new LRU(10000); + this.pathMapCache = new LRU(100000); // Try to optimize for up to 1m addresses. // We use a regular bloom filter here @@ -149,10 +165,10 @@ function WalletDB(options) { // degrades. // Memory used: 1.7mb this.filter = this.options.useFilter !== false - ? bcoin.bloom.fromRate(1000000, 0.001, -1) + ? Bloom.fromRate(1000000, 0.001, -1) : null; - this.db = bcoin.ldb({ + this.db = ldb({ location: this.options.location, db: this.options.db, maxOpenFiles: this.options.maxFiles, @@ -161,9 +177,6 @@ function WalletDB(options) { bufferKeys: !utils.isBrowser }); - if (bcoin.useWorkers) - this.workerPool = new bcoin.workers(); - this._init(); } @@ -182,120 +195,66 @@ WalletDB.layout = layout; */ WalletDB.prototype._init = function _init() { - var self = this; - - if (bcoin.useWorkers) { - this.workerPool.on('error', function(err) { - self.emit('error', err); - }); - } -}; - -/** - * Invoke wallet read mutex lock. - * @private - */ - -WalletDB.prototype._lockRead = function _lockRead(key, func, args, force) { - return this.readLock.lock(key, func, args, force); -}; - -/** - * Invoke wallet write mutex lock. - * @private - */ - -WalletDB.prototype._lockWrite = function _lockWrite(key, func, args, force) { - return this.writeLock.lock(key, func, args, force); -}; - -/** - * Invoke tx handling mutex lock. - * @private - */ - -WalletDB.prototype._lockTX = function _lockTX(func, args, force) { - return this.txLock.lock(func, args, force); + ; }; /** * Open the walletdb, wait for the database to load. * @alias WalletDB#open - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype._open = function open(callback) { - var self = this; +WalletDB.prototype._open = co(function* open() { + yield this.db.open(); + yield this.db.checkVersion('V', 3); + yield this.writeGenesis(); - this.db.open(function(err) { - if (err) - return callback(err); + this.depth = yield this.getDepth(); - self.db.checkVersion('V', 2, function(err) { - if (err) - return callback(err); + this.logger.info( + 'WalletDB loaded (depth=%d, height=%d).', + this.depth, this.height); - self.writeGenesis(function(err) { - if (err) - return callback(err); - - self.getDepth(function(err, depth) { - if (err) - return callback(err); - - self.depth = depth; - - self.logger.info( - 'WalletDB loaded (depth=%d, height=%d).', - depth, self.height); - - self.loadFilter(callback); - }); - }); - }); - }); -}; + yield this.loadFilter(); +}); /** * Close the walletdb, wait for the database to close. * @alias WalletDB#close - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype._close = function close(callback) { - var self = this; +WalletDB.prototype._close = co(function* close() { var keys = Object.keys(this.wallets); - var wallet; + var i, key, wallet; - utils.forEachSerial(keys, function(key, next) { - wallet = self.wallets[key]; - wallet.destroy(next); - }, function(err) { - if (err) - return callback(err); + for (i = 0; i < keys.length; i++) { + key = keys[i]; + wallet = this.wallets[key]; + yield wallet.destroy(); + } - self.db.close(callback); - }); -}; + yield this.db.close(); +}); /** * Backup the wallet db. * @param {String} path - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.backup = function backup(path, callback) { - this.db.backup(path, callback); +WalletDB.prototype.backup = function backup(path) { + return this.db.backup(path); }; /** * Get current wallet wid depth. * @private - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.getDepth = function getDepth(callback) { - var iter, depth; +WalletDB.prototype.getDepth = co(function* getDepth() { + var iter, item, depth; // This may seem like a strange way to do // this, but updating a global state when @@ -312,26 +271,17 @@ WalletDB.prototype.getDepth = function getDepth(callback) { reverse: true }); - iter.next(function(err, key, value) { - if (err) { - return iter.end(function() { - callback(err); - }); - } + item = yield iter.next(); - iter.end(function(err) { - if (err) - return callback(err); + if (!item) + return 1; - if (key === undefined) - return callback(null, 1); + yield iter.end(); - depth = layout.ww(key); + depth = layout.ww(item.key); - callback(null, depth + 1); - }); - }); -}; + return depth + 1; +}); /** * Start batch. @@ -339,10 +289,10 @@ WalletDB.prototype.getDepth = function getDepth(callback) { * @param {WalletID} wid */ -WalletDB.prototype.start = function start(wid) { - assert(utils.isNumber(wid), 'Bad ID for batch.'); - assert(!this.batches[wid], 'Batch already started.'); - this.batches[wid] = this.db.batch(); +WalletDB.prototype.start = function start(wallet) { + assert(!wallet.current, 'Batch already started.'); + wallet.current = this.db.batch(); + return wallet.current; }; /** @@ -351,10 +301,10 @@ WalletDB.prototype.start = function start(wid) { * @param {WalletID} wid */ -WalletDB.prototype.drop = function drop(wid) { - var batch = this.batch(wid); +WalletDB.prototype.drop = function drop(wallet) { + var batch = this.batch(wallet); + wallet.current = null; batch.clear(); - delete this.batches[wid]; }; /** @@ -364,48 +314,51 @@ WalletDB.prototype.drop = function drop(wid) { * @returns {Leveldown.Batch} */ -WalletDB.prototype.batch = function batch(wid) { - var batch; - assert(utils.isNumber(wid), 'Bad ID for batch.'); - batch = this.batches[wid]; - assert(batch, 'Batch does not exist.'); - return batch; +WalletDB.prototype.batch = function batch(wallet) { + assert(wallet.current, 'Batch does not exist.'); + return wallet.current; }; /** * Save batch. * @private * @param {WalletID} wid - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.commit = function commit(wid, callback) { - var batch = this.batch(wid); - delete this.batches[wid]; - batch.write(callback); +WalletDB.prototype.commit = function commit(wallet) { + var batch = wallet.current; + wallet.current = null; + return batch.write(); }; /** * Load the bloom filter into memory. * @private - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.loadFilter = function loadFilter(callback) { - var self = this; +WalletDB.prototype.loadFilter = co(function* loadFilter() { + var iter, item, hash; if (!this.filter) - return callback(); + return; - this.db.iterate({ + iter = this.db.iterator({ gte: layout.p(constants.NULL_HASH), - lte: layout.p(constants.HIGH_HASH), - parse: function(key) { - var hash = layout.pp(key); - self.filter.add(hash, 'hex'); - } - }, callback); -}; + lte: layout.p(constants.HIGH_HASH) + }); + + for (;;) { + item = yield iter.next(); + + if (!item) + break; + + hash = layout.pp(item.key); + this.filter.add(hash, 'hex'); + } +}); /** * Test the bloom filter against an array of address hashes. @@ -414,39 +367,20 @@ WalletDB.prototype.loadFilter = function loadFilter(callback) { * @returns {Boolean} */ -WalletDB.prototype.testFilter = function testFilter(hashes) { - var i; - +WalletDB.prototype.testFilter = function testFilter(hash) { if (!this.filter) return true; - for (i = 0; i < hashes.length; i++) { - if (this.filter.test(hashes[i], 'hex')) - return true; - } - - return false; + return this.filter.test(hash, 'hex'); }; /** * Dump database (for debugging). - * @param {Function} callback - Returns [Error, Object]. + * @returns {Promise} - Returns Object. */ -WalletDB.prototype.dump = function dump(callback) { - var records = {}; - this.db.each({ - gte: ' ', - lte: '~', - values: true - }, function(key, value, next) { - records[key] = value; - next(); - }, function(err) { - if (err) - return callback(err); - callback(null, records); - }); +WalletDB.prototype.dump = function dump() { + return this.db.dump(); }; /** @@ -473,988 +407,879 @@ WalletDB.prototype.unregister = function unregister(wallet) { /** * Map wallet label to wallet id. * @param {String} label - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.getWalletID = function getWalletID(id, callback) { - var self = this; - var wid; +WalletDB.prototype.getWalletID = co(function* getWalletID(id) { + var wid, data; if (!id) - return callback(); + return; if (typeof id === 'number') - return callback(null, id); + return id; - wid = this.walletCache.get(id); + wid = this.widCache.get(id); if (wid) - return callback(null, wid); - - this.db.fetch(layout.l(id), function(data) { - wid = data.readUInt32LE(0, true); - self.walletCache.set(id, wid); return wid; - }, callback); -}; + + data = yield this.db.get(layout.l(id)); + + if (!data) + return; + + wid = data.readUInt32LE(0, true); + + this.widCache.set(id, wid); + + return wid; +}); /** * Get a wallet from the database, setup watcher. * @param {WalletID} wid - * @param {Function} callback - Returns [Error, {@link Wallet}]. + * @returns {Promise} - Returns {@link Wallet}. */ -WalletDB.prototype.get = function get(wid, callback) { - var self = this; +WalletDB.prototype.get = co(function* get(id) { + var wid = yield this.getWalletID(id); + var unlock; - this.getWalletID(wid, function(err, wid) { - if (err) - return callback(err); - - if (!wid) - return callback(); - - self._get(wid, function(err, wallet, watched) { - if (err) - return callback(err); - - if (!wallet) - return callback(); - - if (watched) - return callback(null, wallet); - - try { - self.register(wallet); - } catch (e) { - return callback(e); - } - - wallet.open(function(err) { - if (err) - return callback(err); - - callback(null, wallet); - }); - }); - }); -}; - -/** - * Get a wallet from the database, do not setup watcher. - * @private - * @param {WalletID} wid - * @param {Function} callback - Returns [Error, {@link Wallet}]. - */ - -WalletDB.prototype._get = function get(wid, callback) { - var self = this; - var wallet; - - callback = this._lockRead(wid, get, [wid, callback]); - - if (!callback) + if (!wid) return; - wallet = this.wallets[wid]; + unlock = yield this.readLock.lock(wid); + + try { + return yield this._get(wid); + } finally { + unlock(); + } +}); + +/** + * Get a wallet from the database without a lock. + * @private + * @param {WalletID} wid + * @returns {Promise} - Returns {@link Wallet}. + */ + +WalletDB.prototype._get = co(function* get(wid) { + var wallet = this.wallets[wid]; + var data; if (wallet) - return callback(null, wallet, true); + return wallet; - this.db.fetch(layout.w(wid), function(data) { - return bcoin.wallet.fromRaw(self, data); - }, callback); -}; + data = yield this.db.get(layout.w(wid)); + + if (!data) + return; + + wallet = Wallet.fromRaw(this, data); + + yield wallet.open(); + + this.register(wallet); + + return wallet; +}); /** * Save a wallet to the database. * @param {Wallet} wallet - * @param {Function} callback */ WalletDB.prototype.save = function save(wallet) { - var batch = this.batch(wallet.wid); - var wid = new Buffer(4); - this.walletCache.set(wallet.id, wallet.wid); - batch.put(layout.w(wallet.wid), wallet.toRaw()); - wid.writeUInt32LE(wallet.wid, 0, true); - batch.put(layout.l(wallet.id), wid); + 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); +}; + +/** + * Rename a wallet. + * @param {Wallet} wallet + * @param {String} id + * @returns {Promise} + */ + +WalletDB.prototype.rename = co(function* rename(wallet, id) { + var unlock = yield this.writeLock.lock(); + try { + return yield this._rename(wallet, id); + } finally { + unlock(); + } +}); + +/** + * Rename a wallet without a lock. + * @private + * @param {Wallet} wallet + * @param {String} id + * @returns {Promise} + */ + +WalletDB.prototype._rename = co(function* _rename(wallet, id) { + var old = wallet.id; + var i, paths, path, batch; + + if (!utils.isName(id)) + throw new Error('Bad wallet ID.'); + + if (yield this.has(id)) + throw new Error('ID not available.'); + + batch = this.start(wallet); + batch.del(layout.l(old)); + + wallet.id = id; + + this.save(wallet); + + yield this.commit(wallet); + + this.widCache.remove(old); + + paths = wallet.pathCache.values(); + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + path.id = id; + } +}); + +/** + * Rename an account. + * @param {Account} account + * @param {String} name + */ + +WalletDB.prototype.renameAccount = function renameAccount(account, name) { + var wallet = account.wallet; + var batch = this.batch(wallet); + batch.del(layout.i(account.wid, account.name)); + account.name = name; + this.saveAccount(account); }; /** * Test an api key against a wallet's api key. * @param {WalletID} wid * @param {String} token - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.auth = function auth(wid, token, callback) { - this.get(wid, function(err, wallet) { - if (err) - return callback(err); +WalletDB.prototype.auth = co(function* auth(wid, token) { + var wallet = yield this.get(wid); + if (!wallet) + return; - if (!wallet) - return callback(); + if (typeof token === 'string') { + if (!utils.isHex(token)) + throw new Error('Authentication error.'); + token = new Buffer(token, 'hex'); + } - if (typeof token === 'string') { - if (!utils.isHex(token)) - return callback(new Error('Authentication error.')); - token = new Buffer(token, 'hex'); - } + // Compare in constant time: + if (!crypto.ccmp(token, wallet.token)) + throw new Error('Authentication error.'); - // Compare in constant time: - if (!crypto.ccmp(token, wallet.token)) - return callback(new Error('Authentication error.')); - - callback(null, wallet); - }); -}; + return wallet; +}); /** * Create a new wallet, save to database, setup watcher. * @param {Object} options - See {@link Wallet}. - * @param {Function} callback - Returns [Error, {@link Wallet}]. + * @returns {Promise} - Returns {@link Wallet}. */ -WalletDB.prototype.create = function create(options, callback) { - var self = this; +WalletDB.prototype.create = co(function* create(options) { + var unlock = yield this.writeLock.lock(); + + if (!options) + options = {}; + + try { + return yield this._create(options); + } finally { + unlock(); + } +}); + +/** + * Create a new wallet, save to database without a lock. + * @private + * @param {Object} options - See {@link Wallet}. + * @returns {Promise} - Returns {@link Wallet}. + */ + +WalletDB.prototype._create = co(function* create(options) { + var exists = yield this.has(options.id); var wallet; - if (typeof options === 'function') { - callback = options; - options = {}; - } + if (exists) + throw new Error('Wallet already exists.'); - callback = this._lockWrite(options.id, create, [options, callback]); + wallet = Wallet.fromOptions(this, options); + wallet.wid = this.depth++; - if (!callback) - return; + yield wallet.init(options); - this.has(options.id, function(err, exists) { - if (err) - return callback(err); + this.register(wallet); - if (err) - return callback(err); + this.logger.info('Created wallet %s.', wallet.id); - if (exists) - return callback(new Error('Wallet already exists.')); - - try { - wallet = bcoin.wallet.fromOptions(self, options); - wallet.wid = self.depth++; - } catch (e) { - return callback(e); - } - - try { - self.register(wallet); - } catch (e) { - return callback(e); - } - - wallet.init(options, function(err) { - if (err) - return callback(err); - - self.logger.info('Created wallet %s.', wallet.id); - - callback(null, wallet); - }); - }); -}; + return wallet; +}); /** * Test for the existence of a wallet. * @param {WalletID} id - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.has = function has(id, callback) { - this.getWalletID(id, function(err, wid) { - if (err) - return callback(err); - callback(null, wid != null); - }); -}; +WalletDB.prototype.has = co(function* has(id) { + var wid = yield this.getWalletID(id); + return wid != null; +}); /** * Attempt to create wallet, return wallet if already exists. * @param {Object} options - See {@link Wallet}. - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.ensure = function ensure(options, callback) { - var self = this; - - this.get(options.id, function(err, wallet) { - if (err) - return callback(err); - - if (wallet) - return callback(null, wallet); - - self.create(options, callback); - }); -}; +WalletDB.prototype.ensure = co(function* ensure(options) { + var wallet = yield this.get(options.id); + if (wallet) + return wallet; + return yield this.create(options); +}); /** - * Get an account from the database. - * @param {WalletID} wid - * @param {String|Number} name - Account name/index. - * @param {Function} callback - Returns [Error, {@link Wallet}]. - */ - -WalletDB.prototype.getAccount = function getAccount(wid, name, callback) { - var self = this; - - this.getAccountIndex(wid, name, function(err, index) { - if (err) - return callback(err); - - if (index === -1) - return callback(); - - self._getAccount(wid, index, function(err, account) { - if (err) - return callback(err); - - if (!account) - return callback(); - - account.open(function(err) { - if (err) - return callback(err); - - callback(null, account); - }); - }); - }); -}; - -/** - * Get an account from the database. Do not setup watcher. + * Get an account from the database by wid. * @private * @param {WalletID} wid * @param {Number} index - Account index. - * @param {Function} callback - Returns [Error, {@link Wallet}]. + * @returns {Promise} - Returns {@link Wallet}. */ -WalletDB.prototype._getAccount = function getAccount(wid, index, callback) { - var self = this; - var key = wid + '/' + index; - var account = this.accountCache.get(key); +WalletDB.prototype.getAccount = co(function* getAccount(wid, index) { + var data = yield this.db.get(layout.a(wid, index)); - if (account) - return callback(null, account); + if (!data) + return; - this.db.fetch(layout.a(wid, index), function(data) { - account = bcoin.account.fromRaw(self, data); - self.accountCache.set(key, account); - return account; - }, callback); -}; + return Account.fromRaw(this, data); +}); /** * List account names and indexes from the db. * @param {WalletID} wid - * @param {Function} callback - Returns [Error, Array]. + * @returns {Promise} - Returns Array. */ -WalletDB.prototype.getAccounts = function getAccounts(wid, callback) { +WalletDB.prototype.getAccounts = co(function* getAccounts(wid) { var map = []; - var i, accounts; + var i, items, item, name, index, accounts; - this.db.iterate({ - gte: layout.i(wid, ''), - lte: layout.i(wid, MAX_POINT), - values: true, - parse: function(key, value) { - var name = layout.ii(key)[1]; - var index = value.readUInt32LE(0, true); - map[index] = name; - } - }, function(err) { - if (err) - return callback(err); - - // Get it out of hash table mode. - accounts = []; - - for (i = 0; i < map.length; i++) { - assert(map[i] != null); - accounts.push(map[i]); - } - - callback(null, accounts); + items = yield this.db.range({ + gte: layout.i(wid, '\x00'), + lte: layout.i(wid, '\xff') }); -}; + + for (i = 0; i < items.length; i++) { + item = items[i]; + name = layout.ii(item.key)[1]; + index = item.value.readUInt32LE(0, true); + map[index] = name; + } + + // Get it out of hash table mode. + accounts = []; + + for (i = 0; i < map.length; i++) { + assert(map[i] != null); + accounts.push(map[i]); + } + + return accounts; +}); /** * Lookup the corresponding account name's index. * @param {WalletID} wid * @param {String|Number} name - Account name/index. - * @param {Function} callback - Returns [Error, Number]. + * @returns {Promise} - Returns Number. */ -WalletDB.prototype.getAccountIndex = function getAccountIndex(wid, name, callback) { - if (!wid) - return callback(null, -1); +WalletDB.prototype.getAccountIndex = co(function* getAccountIndex(wid, name) { + var index = yield this.db.get(layout.i(wid, name)); - if (name == null) - return callback(null, -1); + if (!index) + return -1; - if (typeof name === 'number') - return callback(null, name); - - this.db.get(layout.i(wid, name), function(err, index) { - if (err) - return callback(err); - - if (!index) - return callback(null, -1); - - callback(null, index.readUInt32LE(0, true)); - }); -}; + return index.readUInt32LE(0, true); +}); /** * Save an account to the database. * @param {Account} account - * @param {Function} callback + * @returns {Promise} */ WalletDB.prototype.saveAccount = function saveAccount(account) { - var batch = this.batch(account.wid); - var index = new Buffer(4); - var key = account.wid + '/' + account.accountIndex; + var wid = account.wid; + var wallet = account.wallet; + var index = account.accountIndex; + var name = account.name; + var batch = this.batch(account.wallet); + var buf = new Buffer(4); - index.writeUInt32LE(account.accountIndex, 0, true); + buf.writeUInt32LE(index, 0, true); - batch.put(layout.a(account.wid, account.accountIndex), account.toRaw()); - batch.put(layout.i(account.wid, account.name), index); + batch.put(layout.a(wid, index), account.toRaw()); + batch.put(layout.i(wid, name), buf); - this.accountCache.set(key, account); -}; - -/** - * Create an account. - * @param {Object} options - See {@link Account} options. - * @param {Function} callback - Returns [Error, {@link Account}]. - */ - -WalletDB.prototype.createAccount = function createAccount(options, callback) { - var self = this; - var account; - - this.hasAccount(options.wid, options.name, function(err, exists) { - if (err) - return callback(err); - - if (err) - return callback(err); - - if (exists) - return callback(new Error('Account already exists.')); - - try { - account = bcoin.account.fromOptions(self, options); - } catch (e) { - return callback(e); - } - - account.init(function(err) { - if (err) - return callback(err); - - self.logger.info('Created account %s/%s/%d.', - account.id, - account.name, - account.accountIndex); - - callback(null, account); - }); - }); + wallet.accountCache.set(index, account); }; /** * Test for the existence of an account. * @param {WalletID} wid - * @param {String|Number} account - * @param {Function} callback - Returns [Error, Boolean]. + * @param {String|Number} acct + * @returns {Promise} - Returns Boolean. */ -WalletDB.prototype.hasAccount = function hasAccount(wid, account, callback) { - var self = this; - var key; +WalletDB.prototype.hasAccount = co(function* hasAccount(wid, index) { + return yield this.db.has(layout.a(wid, index)); +}); - if (!wid) - return callback(null, false); +/** + * Lookup the corresponding account name's index. + * @param {WalletID} wid + * @param {String|Number} name - Account name/index. + * @returns {Promise} - Returns Number. + */ - this.getAccountIndex(wid, account, function(err, index) { - if (err) - return callback(err); +WalletDB.prototype.getWalletsByHash = co(function* getWalletsByHash(hash) { + var wallets = this.pathMapCache.get(hash); + var data; - if (index === -1) - return callback(null, false); + if (wallets) + return wallets; - key = wid + '/' + index; + data = yield this.db.get(layout.p(hash)); - if (self.accountCache.has(key)) - return callback(null, true); + if (!data) + return; - self.db.has(layout.a(wid, index), callback); - }); + wallets = parseWallets(data); + + this.pathMapCache.get(hash, wallets); + + return wallets; +}); + +/** + * Save an address to the path map. + * @param {WalletID} wid + * @param {KeyRing[]} ring + * @returns {Promise} + */ + +WalletDB.prototype.saveKey = function saveKey(wallet, ring) { + return this.savePath(wallet, ring.toPath()); }; /** - * Save addresses to the path map. + * Save a path to the path map. + * * The path map exists in the form of: - * `p/[address-hash] -> {walletid1=path1, walletid2=path2, ...}` + * - `p[address-hash] -> wids` + * - `P[wid][address-hash] -> path` + * * @param {WalletID} wid - * @param {KeyRing[]} rings - * @param {Function} callback + * @param {Path[]} path + * @returns {Promise} */ -WalletDB.prototype.saveAddress = function saveAddress(wid, rings, callback) { - var self = this; - var items = []; - var i, ring, path; - - for (i = 0; i < rings.length; i++) { - ring = rings[i]; - path = ring.path; - - items.push([ring.getAddress(), path]); - - if (ring.witness) { - path = path.clone(); - path.hash = ring.getProgramHash('hex'); - items.push([ring.getProgramAddress(), path]); - } - } - - utils.forEachSerial(items, function(item, next) { - self.writeAddress(wid, item[0], item[1], next); - }, callback); -}; - -/** - * Save a single address to the path map. - * @param {WalletID} wid - * @param {KeyRing} rings - * @param {Path} path - * @param {Function} callback - */ - -WalletDB.prototype.writeAddress = function writeAddress(wid, address, path, callback) { - var self = this; - var hash = address.getHash('hex'); - var batch = this.batch(wid); +WalletDB.prototype.savePath = co(function* savePath(wallet, path) { + var wid = wallet.wid; + var hash = path.hash; + var batch = this.batch(wallet); + var wallets, result; if (this.filter) this.filter.add(hash, 'hex'); - this.emit('save address', address, path); + this.emit('path', path); - this.getAddressPaths(hash, function(err, paths) { - if (err) - return callback(err); + wallets = yield this.getWalletsByHash(hash); - if (!paths) - paths = {}; + if (!wallets) + wallets = []; - if (paths[wid]) - return callback(); + // Keep these motherfuckers sorted. + result = utils.binaryInsert(wallets, wid, cmp, true); - paths[wid] = path; + if (result === -1) + return; - self.pathCache.set(hash, paths); + this.pathMapCache.set(hash, wallets); + wallet.pathCache.set(hash, path); - batch.put(layout.p(hash), serializePaths(paths)); - - callback(); - }); -}; + batch.put(layout.p(hash), serializeWallets(wallets)); + batch.put(layout.P(wid, hash), path.toRaw()); +}); /** - * Retrieve paths by hash. - * @param {Hash} hash - * @param {Function} callback - */ - -WalletDB.prototype.getAddressPaths = function getAddressPaths(hash, callback) { - var self = this; - var paths; - - if (!hash) - return callback(); - - paths = this.pathCache.get(hash); - - if (paths) - return callback(null, paths); - - this.db.fetch(layout.p(hash), function(value) { - return parsePaths(value, hash); - }, function(err, paths) { - if (err) - return callback(err); - - if (!paths) - return callback(); - - self.pathCache.set(hash, paths); - - callback(null, paths); - }); -}; - -/** - * Test whether an address hash exists in the - * path map and is relevant to the wallet id. + * Retrieve path by hash. * @param {WalletID} wid * @param {Hash} hash - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.hasAddress = function hasAddress(wid, hash, callback) { - this.getAddressPaths(hash, function(err, paths) { - if (err) - return callback(err); +WalletDB.prototype.getPath = co(function* getPath(wid, hash) { + var data = yield this.db.get(layout.P(wid, hash)); + var path; - if (!paths || !paths[wid]) - return callback(null, false); + if (!data) + return; - callback(null, true); + path = Path.fromRaw(data); + path.wid = wid; + path.hash = hash; + + return path; +}); + +/** + * Get all address hashes. + * @returns {Promise} + */ + +WalletDB.prototype.getHashes = function getHashes() { + return this.db.keys({ + gte: layout.p(constants.NULL_HASH), + lte: layout.p(constants.HIGH_HASH), + parse: layout.pp }); }; /** * Get all address hashes. * @param {WalletID} wid - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.getAddressHashes = function getAddressHashes(wid, callback) { - if (!callback) { - callback = wid; - wid = null; - } - - this.db.iterate({ - gte: layout.p(constants.NULL_HASH), - lte: layout.p(constants.HIGH_HASH), - values: true, - parse: function(key, value) { - var paths = parsePaths(value); - - if (wid && !paths[wid]) - return; - - return layout.pp(key); - } - }, callback); +WalletDB.prototype.getWalletHashes = function getWalletHashes(wid) { + return this.db.keys({ + gte: layout.P(wid, constants.NULL_HASH), + lte: layout.P(wid, constants.HIGH_HASH), + parse: layout.Pp + }); }; /** * Get all paths for a wallet. * @param {WalletID} wid - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.getWalletPaths = function getWalletPaths(wid, callback) { - this.db.iterate({ - gte: layout.p(constants.NULL_HASH), - lte: layout.p(constants.HIGH_HASH), - values: true, - parse: function(key, value) { - var hash = layout.pp(key); - var paths = parsePaths(value, hash); - var path = paths[wid]; +WalletDB.prototype.getWalletPaths = co(function* getWalletPaths(wid) { + var i, item, items, hash, path; - if (!path) - return; + items = yield this.db.range({ + gte: layout.P(wid, constants.NULL_HASH), + lte: layout.P(wid, constants.HIGH_HASH) + }); - return path; - } - }, callback); -}; + for (i = 0; i < items.length; i++) { + item = items[i]; + hash = layout.Pp(item.key); + path = Path.fromRaw(item.value); + + path.hash = hash; + path.wid = wid; + + items[i] = path; + } + + return items; +}); /** * Get all wallet ids. - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.getWallets = function getWallets(callback) { - this.db.iterate({ - gte: layout.l(''), - lte: layout.l(MAX_POINT), - parse: function(key) { - return layout.ll(key); - } - }, callback); +WalletDB.prototype.getWallets = function getWallets() { + return this.db.keys({ + gte: layout.l('\x00'), + lte: layout.l('\xff'), + parse: layout.ll + }); }; +/** + * Encrypt all imported keys for a wallet. + * @param {WalletID} wid + * @returns {Promise} + */ + +WalletDB.prototype.encryptKeys = co(function* encryptKeys(wallet, key) { + var wid = wallet.wid; + var paths = yield wallet.getPaths(); + var batch = this.batch(wallet); + var i, path, iv; + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + + if (!path.data) + continue; + + assert(!path.encrypted); + + iv = new Buffer(path.hash, 'hex'); + iv = iv.slice(0, 16); + + path.data = crypto.encipher(path.data, key, iv); + path.encrypted = true; + + wallet.pathCache.set(path.hash, path); + + batch.put(layout.P(wid, path.hash), path.toRaw()); + } +}); + +/** + * Decrypt all imported keys for a wallet. + * @param {WalletID} wid + * @returns {Promise} + */ + +WalletDB.prototype.decryptKeys = co(function* decryptKeys(wallet, key) { + var wid = wallet.wid; + var paths = yield wallet.getPaths(); + var batch = this.batch(wallet); + var i, path, iv; + + for (i = 0; i < paths.length; i++) { + path = paths[i]; + + if (!path.data) + continue; + + assert(path.encrypted); + + iv = new Buffer(path.hash, 'hex'); + iv = iv.slice(0, 16); + + path.data = crypto.decipher(path.data, key, iv); + path.encrypted = false; + + wallet.pathCache.set(path.hash, path); + + batch.put(layout.P(wid, path.hash), path.toRaw()); + } +}); + /** * Rescan the blockchain. * @param {ChainDB} chaindb - * @param {Function} callback + * @param {Number} height + * @returns {Promise} */ -WalletDB.prototype.rescan = function rescan(chaindb, height, callback) { - var self = this; - - if (typeof height === 'function') { - callback = height; - height = null; +WalletDB.prototype.rescan = co(function* rescan(chaindb, height) { + var unlock = yield this.txLock.lock(); + try { + return yield this._rescan(chaindb, height); + } finally { + unlock(); } +}); + +/** + * Rescan the blockchain without a lock. + * @private + * @param {ChainDB} chaindb + * @param {Number} height + * @returns {Promise} + */ + +WalletDB.prototype._rescan = co(function* rescan(chaindb, height) { + var self = this; + var hashes; if (height == null) height = this.height; - callback = this._lockTX(rescan, [chaindb, height, callback]); + hashes = yield this.getHashes(); - if (!callback) - return; + this.logger.info('Scanning for %d addresses.', hashes.length); - this.getAddressHashes(function(err, hashes) { - if (err) - return callback(err); - - self.logger.info('Scanning for %d addresses.', hashes.length); - - chaindb.scan(height, hashes, function(block, txs, next) { - self.addBlock(block, txs, next, true); - }, callback); + yield chaindb.scan(height, hashes, function(block, txs) { + return self._addBlock(block, txs); }); -}; +}); /** * Get keys of all pending transactions * in the wallet db (for resending). - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.getPendingKeys = function getPendingKeys(callback) { +WalletDB.prototype.getPendingKeys = co(function* getPendingKeys() { var layout = require('./txdb').layout; var dummy = new Buffer(0); var uniq = {}; + var keys = []; + var result = []; + var i, iter, item, key, wid, hash; - this.db.iterate({ + iter = yield this.db.iterator({ gte: layout.prefix(0x00000000, dummy), - lte: layout.prefix(0xffffffff, dummy), - keys: true, - parse: function(key) { - var wid, hash; + lte: layout.prefix(0xffffffff, dummy) + }); - if (key[5] !== 0x70) - return; + for (;;) { + item = yield iter.next(); - wid = layout.pre(key); - hash = layout.pp(key); + if (!item) + break; - if (uniq[hash]) - return; + if (item.key[5] === 0x70) + keys.push(item.key); + } - uniq[hash] = true; + for (i = 0; i < keys.length; i++) { + key = keys[i]; - return layout.prefix(wid, layout.t(hash)); - } - }, callback); -}; + wid = layout.pre(key); + hash = layout.pp(key); + + if (uniq[hash]) + continue; + + uniq[hash] = true; + + key = layout.prefix(wid, layout.t(hash)); + result.push(key); + } + + return result; +}); /** * Resend all pending transactions. - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.resend = function resend(callback) { - var self = this; +WalletDB.prototype.resend = co(function* resend() { + var keys = yield this.getPendingKeys(); + var i, key, data, tx; - this.getPendingKeys(function(err, keys) { - if (err) - return callback(err); + if (keys.length > 0) + this.logger.info('Rebroadcasting %d transactions.', keys.length); - if (keys.length > 0) - self.logger.info('Rebroadcasting %d transactions.', keys.length); + for (i = 0; i < keys.length; i++) { + key = keys[i]; + data = yield this.db.get(key); - utils.forEachSerial(keys, function(key, next) { - self.db.fetch(key, function(data) { - return bcoin.tx.fromExtended(data); - }, function(err, tx) { - if (err) - return next(err); + if (!data) + continue; - if (!tx) - return next(); + tx = TX.fromExtended(data); - self.emit('send', tx); - - next(); - }); - }, callback); - }); -}; + this.emit('send', tx); + } +}); /** - * Helper function to get a wallet. - * @private - * @param {WalletID} wid - * @param {Function} callback - * @param {Function} handler + * Get all wallet ids by multiple address hashes. + * @param {Hash[]} hashes + * @returns {Promise} */ -WalletDB.prototype.fetchWallet = function fetchWallet(wid, callback, handler) { - this.get(wid, function(err, wallet) { - if (err) - return callback(err); +WalletDB.prototype.getWidsByHashes = co(function* getWidsByHashes(hashes) { + var result = []; + var i, j, hash, wids; - if (!wallet) - return callback(new Error('No wallet.')); + for (i = 0; i < hashes.length; i++) { + hash = hashes[i]; - handler(wallet, function(err, res1, res2) { - if (err) - return callback(err); + if (!this.testFilter(hash)) + continue; - callback(null, res1, res2); - }); - }); -}; + wids = yield this.getWalletsByHash(hash); -/** - * Map a transactions' addresses to wallet IDs. - * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link PathInfo[]}]. - */ + for (j = 0; j < wids.length; j++) + utils.binaryInsert(result, wids[j], cmp, true); + } -WalletDB.prototype.mapWallets = function mapWallets(tx, callback) { - var self = this; - var hashes = tx.getHashes('hex'); - var wallets; - - if (!this.testFilter(hashes)) - return callback(); - - this.getTable(hashes, function(err, table) { - if (err) - return callback(err); - - if (!table) - return callback(); - - wallets = PathInfo.map(self, tx, table); - - callback(null, wallets); - }); -}; - -/** - * Map a transactions' addresses to wallet IDs. - * @param {TX} tx - * @param {Function} callback - Returns [Error, {@link PathInfo}]. - */ - -WalletDB.prototype.getPathInfo = function getPathInfo(wallet, tx, callback) { - var self = this; - var hashes = tx.getHashes('hex'); - var info; - - this.getTable(hashes, function(err, table) { - if (err) - return callback(err); - - if (!table) - return callback(); - - info = new PathInfo(self, wallet.wid, tx, table); - info.id = wallet.id; - - callback(null, info); - }); -}; - -/** - * Map address hashes to paths. - * @param {Hash[]} hashes - Address hashes. - * @param {Function} callback - Returns [Error, {@link AddressTable}]. - */ - -WalletDB.prototype.getTable = function getTable(hashes, callback) { - var self = this; - var table = {}; - var count = 0; - var i, keys, values; - - utils.forEachSerial(hashes, function(hash, next) { - self.getAddressPaths(hash, function(err, paths) { - if (err) - return next(err); - - if (!paths) { - assert(!table[hash]); - table[hash] = []; - return next(); - } - - keys = Object.keys(paths); - values = []; - - for (i = 0; i < keys.length; i++) - values.push(paths[keys[i]]); - - assert(!table[hash]); - table[hash] = values; - count += values.length; - - next(); - }); - }, function(err) { - if (err) - return callback(err); - - if (count === 0) - return callback(); - - callback(null, table); - }); -}; + return result; +}); /** * Write the genesis block as the best hash. - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.writeGenesis = function writeGenesis(callback) { - var self = this; - - this.getTip(function(err, block) { - if (err) - return callback(err); - - if (block) { - self.tip = block.hash; - self.height = block.height; - return callback(); - } - - self.setTip(self.network.genesis.hash, 0, callback); - }); -}; +WalletDB.prototype.writeGenesis = co(function* writeGenesis() { + var block = yield this.getTip(); + if (block) { + this.tip = block.hash; + this.height = block.height; + return; + } + yield this.setTip(this.network.genesis.hash, 0); +}); /** * Get the best block hash. - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.getTip = function getTip(callback) { - this.db.fetch(layout.R, function(data) { - return WalletBlock.fromTip(data); - }, callback); -}; +WalletDB.prototype.getTip = co(function* getTip() { + var data = yield this.db.get(layout.R); + + if (!data) + return; + + return WalletBlock.fromTip(data); +}); /** * Write the best block hash. * @param {Hash} hash * @param {Number} height - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.setTip = function setTip(hash, height, callback) { - var self = this; +WalletDB.prototype.setTip = co(function* setTip(hash, height) { var block = new WalletBlock(hash, height); - this.db.put(layout.R, block.toTip(), function(err) { - if (err) - return callback(err); - self.tip = block.hash; - self.height = block.height; + yield this.db.put(layout.R, block.toTip()); - callback(); - }); -}; + this.tip = block.hash; + this.height = block.height; +}); /** * Connect a block. * @param {WalletBlock} block - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.writeBlock = function writeBlock(block, matches, callback) { +WalletDB.prototype.writeBlock = function writeBlock(block, matches) { var batch = this.db.batch(); var i, hash, wallets; batch.put(layout.R, block.toTip()); - if (block.hashes.length > 0) { - batch.put(layout.b(block.hash), block.toRaw()); + if (block.hashes.length === 0) + return batch.write(); - for (i = 0; i < block.hashes.length; i++) { - hash = block.hashes[i]; - wallets = matches[i]; - batch.put(layout.e(hash), serializeWallets(wallets)); - } + batch.put(layout.b(block.hash), block.toRaw()); + + for (i = 0; i < block.hashes.length; i++) { + hash = block.hashes[i]; + wallets = matches[i]; + batch.put(layout.e(hash), serializeWallets(wallets)); } - batch.write(callback); + return batch.write(); }; /** * Disconnect a block. * @param {WalletBlock} block - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.unwriteBlock = function unwriteBlock(block, callback) { +WalletDB.prototype.unwriteBlock = function unwriteBlock(block) { var batch = this.db.batch(); var prev = new WalletBlock(block.prevBlock, block.height - 1); batch.put(layout.R, prev.toTip()); batch.del(layout.b(block.hash)); - batch.write(callback); + return batch.write(); }; /** * Get a wallet block (with hashes). * @param {Hash} hash - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.getBlock = function getBlock(hash, callback) { - this.db.fetch(layout.b(hash), function(data) { - return WalletBlock.fromRaw(hash, data); - }, callback); -}; +WalletDB.prototype.getBlock = co(function* getBlock(hash) { + var data = yield this.db.get(layout.b(hash)); + + if (!data) + return; + + return WalletBlock.fromRaw(hash, data); +}); /** * Get a TX->Wallet map. * @param {Hash} hash - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.getWalletsByTX = function getWalletsByTX(hash, callback) { - this.db.fetch(layout.e(hash), parseWallets, callback); -}; +WalletDB.prototype.getWalletsByTX = co(function* getWalletsByTX(hash) { + var data = yield this.db.get(layout.e(hash)); + + if (!data) + return; + + return parseWallets(data); +}); /** * Add a block's transactions and write the new best hash. * @param {ChainEntry} entry - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.addBlock = function addBlock(entry, txs, callback, force) { - var self = this; - var block, matches, hash; +WalletDB.prototype.addBlock = co(function* addBlock(entry, txs) { + var unlock = yield this.txLock.lock(); + try { + return yield this._addBlock(entry, txs); + } finally { + unlock(); + } +}); - callback = this._lockTX(addBlock, [entry, txs, callback], force); +/** + * Add a block's transactions without a lock. + * @private + * @param {ChainEntry} entry + * @returns {Promise} + */ - if (!callback) - return; +WalletDB.prototype._addBlock = co(function* addBlock(entry, txs) { + var i, block, matches, hash, tx, wallets; if (this.options.useCheckpoints) { - if (entry.height <= this.network.checkpoints.lastHeight) - return this.setTip(entry.hash, entry.height, callback); + if (entry.height <= this.network.checkpoints.lastHeight) { + yield this.setTip(entry.hash, entry.height); + return; + } } block = WalletBlock.fromEntry(entry); @@ -1465,480 +1290,188 @@ WalletDB.prototype.addBlock = function addBlock(entry, txs, callback, force) { this.tip = block.hash; this.height = block.height; - // NOTE: Atomicity doesn't matter here. If we crash + // Atomicity doesn't matter here. If we crash // during this loop, the automatic rescan will get // the database back into the correct state. - utils.forEachSerial(txs, function(tx, next) { - self.addTX(tx, function(err, wallets) { - if (err) - return next(err); + for (i = 0; i < txs.length; i++) { + tx = txs[i]; - if (!wallets) - return next(); + wallets = yield this._addTX(tx); - hash = tx.hash('hex'); - block.hashes.push(hash); - matches.push(wallets); + if (!wallets) + continue; - next(); - }, true); - }, function(err) { - if (err) - return callback(err); + hash = tx.hash('hex'); + block.hashes.push(hash); + matches.push(wallets); + } - if (block.hashes.length > 0) { - self.logger.info('Connecting block %s (%d txs).', - utils.revHex(block.hash), block.hashes.length); - } + if (block.hashes.length > 0) { + this.logger.info('Connecting block %s (%d txs).', + utils.revHex(block.hash), block.hashes.length); + } - self.writeBlock(block, matches, callback); - }); -}; + yield this.writeBlock(block, matches); +}); /** * Unconfirm a block's transactions * and write the new best hash (SPV version). * @param {ChainEntry} entry - * @param {Function} callback + * @returns {Promise} */ -WalletDB.prototype.removeBlock = function removeBlock(entry, callback) { - var self = this; - var block; +WalletDB.prototype.removeBlock = co(function* removeBlock(entry) { + var unlock = yield this.txLock.lock(); + try { + return yield this._removeBlock(entry); + } finally { + unlock(); + } +}); - callback = this._lockTX(removeBlock, [entry, callback]); +/** + * Unconfirm a block's transactions. + * @private + * @param {ChainEntry} entry + * @returns {Promise} + */ - if (!callback) - return; +WalletDB.prototype._removeBlock = co(function* removeBlock(entry) { + var block = WalletBlock.fromEntry(entry); + var i, data, hash; - block = WalletBlock.fromEntry(entry); - - // Note: // If we crash during a reorg, there's not much to do. // Reorgs cannot be rescanned. The database will be // in an odd state, with some txs being confirmed // when they shouldn't be. That being said, this // should eventually resolve itself when a new block // comes in. - this.getBlock(block.hash, function(err, data) { - if (err) - return callback(err); + data = yield this.getBlock(block.hash); - if (data) - block.hashes = data.hashes; + if (data) + block.hashes = data.hashes; - if (block.hashes.length > 0) { - self.logger.warning('Disconnecting block %s (%d txs).', - utils.revHex(block.hash), block.hashes.length); - } + if (block.hashes.length > 0) { + this.logger.warning('Disconnecting block %s (%d txs).', + utils.revHex(block.hash), block.hashes.length); + } - // Unwrite the tip as fast as we can. - self.unwriteBlock(block, function(err) { - if (err) - return callback(err); + // Unwrite the tip as fast as we can. + yield this.unwriteBlock(block); - utils.forEachSerial(block.hashes, function(hash, next) { - self.getWalletsByTX(hash, function(err, wallets) { - if (err) - return next(err); + for (i = 0; i < block.hashes.length; i++) { + hash = block.hashes[i]; + yield this._unconfirmTX(hash); + } - if (!wallets) - return next(); - - utils.forEachSerial(wallets, function(wid, next) { - self.get(wid, function(err, wallet) { - if (err) - return next(err); - - if (!wallet) - return next(); - - wallet.tx.unconfirm(hash, next); - }); - }, function(err) { - if (err) - return callback(err); - - self.tip = block.hash; - self.height = block.height; - - next(); - }); - }); - }, callback); - }); - }); -}; + this.tip = block.hash; + this.height = block.height; +}); /** * Add a transaction to the database, map addresses * to wallet IDs, potentially store orphans, resolve * orphans, or confirm a transaction. * @param {TX} tx - * @param {Function} callback - Returns [Error]. + * @returns {Promise} */ -WalletDB.prototype.addTX = function addTX(tx, callback, force) { - var self = this; +WalletDB.prototype.addTX = co(function* addTX(tx) { + var unlock = yield this.txLock.lock(); + try { + return yield this._addTX(tx); + } finally { + unlock(); + } +}); - callback = this._lockTX(addTX, [tx, callback], force); +/** + * Add a transaction to the database without a lock. + * @private + * @param {TX} tx + * @returns {Promise} + */ - if (!callback) - return; +WalletDB.prototype._addTX = co(function* addTX(tx) { + var i, hashes, wallets, wid, wallet; assert(!tx.mutable, 'Cannot add mutable TX to wallet.'); - // Note: // Atomicity doesn't matter here. If we crash, // the automatic rescan will get the database // back in the correct state. - this.mapWallets(tx, function(err, wallets) { - if (err) - return callback(err); - - if (!wallets) - return callback(); - - self.logger.info( - 'Incoming transaction for %d wallets (%s).', - wallets.length, tx.rhash); - - utils.forEachSerial(wallets, function(info, next) { - self.get(info.wid, function(err, wallet) { - if (err) - return next(err); - - if (!wallet) - return next(); - - self.logger.debug('Adding tx to wallet: %s', wallet.id); - - info.id = wallet.id; - - wallet.tx.add(tx, info, function(err) { - if (err) - return next(err); - - wallet.handleTX(info, next); - }); - }); - }, function(err) { - if (err) - return callback(err); - callback(null, wallets); - }); - }); -}; - -/** - * Get the corresponding path for an address hash. - * @param {WalletID} wid - * @param {Hash} hash - * @param {Function} callback - */ - -WalletDB.prototype.getAddressPath = function getAddressPath(wid, hash, callback) { - var path; - this.getAddressPaths(hash, function(err, paths) { - if (err) - return callback(err); - - if (!paths) - return callback(); - - path = paths[wid]; - - if (!path) - return callback(); - - callback(null, path); - }); -}; - -/** - * Path Info - * @constructor - */ - -function PathInfo(db, wid, tx, table) { - if (!(this instanceof PathInfo)) - return new PathInfo(db, wid, tx, table); - - // Reference to the walletdb. - this.db = db; - - // All relevant Accounts for - // inputs and outputs (for database indexing). - this.accounts = []; - - // All output paths (for deriving during sync). - this.paths = []; - - // Wallet ID - this.wid = wid; - - // Wallet Label (passed in by caller). - this.id = null; - - // Map of address hashes->paths (for everything). - this.table = null; - - // Map of address hashes->paths (specific to wallet). - this.pathMap = {}; - - // Current transaction. - this.tx = null; - - // Wallet-specific details cache. - this._details = null; - this._json = null; - - if (tx) - this.fromTX(tx, table); -} - -PathInfo.map = function map(db, tx, table) { - var hashes = Object.keys(table); - var wallets = []; - var info = []; - var uniq = {}; - var i, j, hash, paths, path, wid; - - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - paths = table[hash]; - for (j = 0; j < paths.length; j++) { - path = paths[j]; - if (!uniq[path.wid]) { - uniq[path.wid] = true; - wallets.push(path.wid); - } - } - } + hashes = tx.getHashes('hex'); + wallets = yield this.getWidsByHashes(hashes); if (wallets.length === 0) return; + this.logger.info( + 'Incoming transaction for %d wallets (%s).', + wallets.length, tx.rhash); + for (i = 0; i < wallets.length; i++) { wid = wallets[i]; - info.push(new PathInfo(db, wid, tx, table)); + wallet = yield this.get(wid); + + if (!wallet) + continue; + + this.logger.debug('Adding tx to wallet: %s', wallet.id); + + yield wallet.add(tx); } - return info; -}; - -PathInfo.prototype.fromTX = function fromTX(tx, table) { - var uniq = {}; - var i, j, hashes, hash, paths, path; - - this.tx = tx; - this.table = table; - - hashes = Object.keys(table); - - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - paths = table[hash]; - for (j = 0; j < paths.length; j++) { - path = paths[j]; - if (path.wid !== this.wid) - continue; - this.pathMap[hash] = path; - if (!uniq[path.account]) { - uniq[path.account] = true; - this.accounts.push(path.account); - } - } - } - - hashes = tx.getOutputHashes('hex'); - - for (i = 0; i < hashes.length; i++) { - hash = hashes[i]; - paths = table[hash]; - for (j = 0; j < paths.length; j++) { - path = paths[j]; - if (path.wid !== this.wid) - continue; - this.paths.push(path); - } - } - - return this; -}; - -PathInfo.fromTX = function fromTX(db, wid, tx, table) { - return new PathInfo(db, wid).fromTX(tx, table); -}; + return wallets; +}); /** - * Test whether the map has paths - * for a given address hash. + * Unconfirm a transaction from all relevant wallets. * @param {Hash} hash - * @returns {Boolean} + * @returns {Promise} */ -PathInfo.prototype.hasPath = function hasPath(hash) { - if (!hash) - return false; - - return this.pathMap[hash] != null; -}; +WalletDB.prototype.unconfirmTX = co(function* unconfirmTX(hash) { + var unlock = yield this.txLock.lock(); + try { + return yield this._unconfirmTX(hash); + } finally { + unlock(); + } +}); /** - * Get paths for a given address hash. + * Unconfirm a transaction from all + * relevant wallets without a lock. + * @private * @param {Hash} hash - * @returns {Path[]|null} + * @returns {Promise} */ -PathInfo.prototype.getPath = function getPath(hash) { - if (!hash) +WalletDB.prototype._unconfirmTX = co(function* unconfirmTX(hash) { + var wallets = yield this.getWalletsByTX(hash); + var i, wid, wallet; + + if (!wallets) return; - return this.pathMap[hash]; -}; + for (i = 0; i < wallets.length; i++) { + wid = wallets[i]; + wallet = yield this.get(wid); -PathInfo.prototype.toDetails = function toDetails() { - var details = this._details; + if (!wallet) + continue; - if (!details) { - details = new Details(this); - this._details = details; + yield wallet.unconfirm(hash); } - - return details; -}; - -PathInfo.prototype.toJSON = function toJSON() { - var json = this._json; - - if (!json) { - json = this.toDetails().toJSON(); - this._json = json; - } - - return json; -}; - -/** - * Details - * @constructor - */ - -function Details(info) { - if (!(this instanceof Details)) - return new Details(info); - - this.db = info.db; - this.network = info.db.network; - this.wid = info.wid; - this.id = info.id; - this.hash = info.tx.hash('hex'); - this.height = info.tx.height; - this.block = info.tx.block; - this.index = info.tx.index; - this.confirmations = info.tx.getConfirmations(this.db.height); - this.fee = info.tx.getFee(); - this.ts = info.tx.ts; - this.ps = info.tx.ps; - this.tx = info.tx; - this.inputs = []; - this.outputs = []; - - this.init(info.table); -} - -Details.prototype.init = function init(table) { - this._insert(this.tx.inputs, this.inputs, table); - this._insert(this.tx.outputs, this.outputs, table); -}; - -Details.prototype._insert = function _insert(vector, target, table) { - var i, j, io, address, hash, paths, path, member; - - for (i = 0; i < vector.length; i++) { - io = vector[i]; - member = new DetailsMember(); - - if (io instanceof bcoin.input) - member.value = io.coin ? io.coin.value : 0; - else - member.value = io.value; - - address = io.getAddress(); - - if (address) { - member.address = address; - - hash = address.getHash('hex'); - paths = table[hash]; - - for (j = 0; j < paths.length; j++) { - path = paths[j]; - if (path.wid === this.wid) { - path.id = this.id; - member.path = path; - break; - } - } - } - - target.push(member); - } -}; - -Details.prototype.toJSON = function toJSON() { - var self = this; - return { - wid: this.wid, - id: this.id, - hash: utils.revHex(this.hash), - height: this.height, - block: this.block ? utils.revHex(this.block) : null, - ts: this.ts, - ps: this.ps, - index: this.index, - fee: utils.btc(this.fee), - confirmations: this.confirmations, - inputs: this.inputs.map(function(input) { - return input.toJSON(self.network); - }), - outputs: this.outputs.map(function(output) { - return output.toJSON(self.network); - }), - tx: this.tx.toRaw().toString('hex') - }; -}; - -/** - * DetailsMember - * @constructor - */ - -function DetailsMember() { - if (!(this instanceof DetailsMember)) - return new DetailsMember(); - - this.value = 0; - this.address = null; - this.path = null; -} - -DetailsMember.prototype.toJSON = function toJSON(network) { - return { - value: utils.btc(this.value), - address: this.address - ? this.address.toBase58(network) - : null, - path: this.path - ? this.path.toJSON() - : null - }; -}; +}); /** * Wallet Block * @constructor + * @param {Hash} hash + * @param {Number} height */ function WalletBlock(hash, height) { @@ -1951,6 +1484,12 @@ function WalletBlock(hash, height) { this.hashes = []; } +/** + * Instantiate wallet block from chain entry. + * @private + * @param {ChainEntry} entry + */ + WalletBlock.prototype.fromEntry = function fromEntry(entry) { this.hash = entry.hash; this.height = entry.height; @@ -1958,6 +1497,12 @@ WalletBlock.prototype.fromEntry = function fromEntry(entry) { return this; }; +/** + * Instantiate wallet block from json object. + * @private + * @param {Object} json + */ + WalletBlock.prototype.fromJSON = function fromJSON(json) { this.hash = utils.revHex(json.hash); this.height = json.height; @@ -1966,6 +1511,13 @@ WalletBlock.prototype.fromJSON = function fromJSON(json) { return this; }; +/** + * Instantiate wallet block from serialized data. + * @private + * @param {Hash} hash + * @param {Buffer} data + */ + WalletBlock.prototype.fromRaw = function fromRaw(hash, data) { var p = new BufferReader(data); this.hash = hash; @@ -1975,6 +1527,12 @@ WalletBlock.prototype.fromRaw = function fromRaw(hash, data) { return this; }; +/** + * Instantiate wallet block from serialized tip data. + * @private + * @param {Buffer} data + */ + WalletBlock.prototype.fromTip = function fromTip(data) { var p = new BufferReader(data); this.hash = p.readHash('hex'); @@ -1982,22 +1540,52 @@ WalletBlock.prototype.fromTip = function fromTip(data) { return this; }; +/** + * Instantiate wallet block from chain entry. + * @param {ChainEntry} entry + * @returns {WalletBlock} + */ + WalletBlock.fromEntry = function fromEntry(entry) { return new WalletBlock().fromEntry(entry); }; +/** + * Instantiate wallet block from json object. + * @param {Object} json + * @returns {WalletBlock} + */ + WalletBlock.fromJSON = function fromJSON(json) { return new WalletBlock().fromJSON(json); }; +/** + * Instantiate wallet block from serialized data. + * @param {Hash} hash + * @param {Buffer} data + * @returns {WalletBlock} + */ + WalletBlock.fromRaw = function fromRaw(hash, data) { return new WalletBlock().fromRaw(hash, data); }; +/** + * Instantiate wallet block from serialized tip data. + * @private + * @param {Buffer} data + */ + WalletBlock.fromTip = function fromTip(data) { return new WalletBlock().fromTip(data); }; +/** + * Serialize the wallet block as a tip (hash and height). + * @returns {Buffer} + */ + WalletBlock.prototype.toTip = function toTip() { var p = new BufferWriter(); p.writeHash(this.hash); @@ -2005,6 +1593,12 @@ WalletBlock.prototype.toTip = function toTip() { return p.render(); }; +/** + * Serialize the wallet block as a block. + * Contains matching transaction hashes. + * @returns {Buffer} + */ + WalletBlock.prototype.toRaw = function toRaw() { var p = new BufferWriter(); var i; @@ -2017,6 +1611,11 @@ WalletBlock.prototype.toRaw = function toRaw() { return p.render(); }; +/** + * Convert the block to a more json-friendly object. + * @returns {Object} + */ + WalletBlock.prototype.toJSON = function toJSON() { return { hash: utils.revHex(this.hash), @@ -2028,35 +1627,6 @@ WalletBlock.prototype.toJSON = function toJSON() { * Helpers */ -function parsePaths(data, hash) { - var p = new BufferReader(data); - var out = {}; - var path; - - while (p.left()) { - path = Path.fromRaw(p); - out[path.wid] = path; - if (hash) - path.hash = hash; - } - - return out; -} - -function serializePaths(out) { - var p = new BufferWriter(); - var keys = Object.keys(out); - var i, wid, path; - - for (i = 0; i < keys.length; i++) { - wid = keys[i]; - path = out[wid]; - path.toRaw(p); - } - - return p.render(); -} - function parseWallets(data) { var p = new BufferReader(data); var wallets = []; @@ -2067,16 +1637,20 @@ function parseWallets(data) { function serializeWallets(wallets) { var p = new BufferWriter(); - var i, info; + var i, wid; for (i = 0; i < wallets.length; i++) { - info = wallets[i]; - p.writeU32(info.wid); + wid = wallets[i]; + p.writeU32(wid); } return p.render(); } +function cmp(a, b) { + return a - b; +} + /* * Expose */ diff --git a/lib/wallet/walletkey.js b/lib/wallet/walletkey.js new file mode 100644 index 00000000..599681e5 --- /dev/null +++ b/lib/wallet/walletkey.js @@ -0,0 +1,307 @@ +/*! + * walletkey.js - walletkey object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var constants = require('../protocol/constants'); +var KeyRing = require('../primitives/keyring'); +var utils = require('../utils/utils'); +var Path = require('./path'); + +/** + * Represents a key ring which amounts to an address. + * @exports WalletKey + * @constructor + * @param {Object} options + */ + +function WalletKey(options, network) { + if (!(this instanceof WalletKey)) + return new WalletKey(options, network); + + KeyRing.call(this, options, network); + + this.keyType = Path.types.HD; + + this.id = null; + this.wid = -1; + this.name = null; + this.account = -1; + this.branch = -1; + this.index = -1; +} + +utils.inherits(WalletKey, KeyRing); + +/** + * Instantiate key ring from options. + * @param {Object} options + * @returns {WalletKey} + */ + +WalletKey.fromOptions = function fromOptions(options) { + return new WalletKey().fromOptions(options); +}; + +/** + * Instantiate wallet key from a private key. + * @param {Buffer} key + * @param {Boolean?} compressed + * @param {(NetworkType|Network}) network + * @returns {WalletKey} + */ + +WalletKey.fromPrivate = function fromPrivate(key, compressed, network) { + return new WalletKey().fromPrivate(key, compressed, network); +}; + +/** + * Generate a wallet key. + * @param {(Network|NetworkType)?} network + * @returns {WalletKey} + */ + +WalletKey.generate = function(compressed, network) { + return new WalletKey().generate(compressed, network); +}; + +/** + * Instantiate wallet key from a public key. + * @param {Buffer} publicKey + * @param {(NetworkType|Network}) network + * @returns {WalletKey} + */ + +WalletKey.fromPublic = function fromPublic(key, network) { + return new WalletKey().fromPublic(key, network); +}; + +/** + * Instantiate wallet key from a public key. + * @param {Buffer} publicKey + * @param {(NetworkType|Network}) network + * @returns {WalletKey} + */ + +WalletKey.fromKey = function fromKey(key, compressed, network) { + return new WalletKey().fromKey(key, compressed, network); +}; + +/** + * Instantiate wallet key from script. + * @param {Buffer} key + * @param {Script} script + * @param {(NetworkType|Network}) network + * @returns {WalletKey} + */ + +WalletKey.fromScript = function fromScript(key, script, compressed, network) { + return new WalletKey().fromScript(key, script, compressed, network); +}; + +/** + * Instantiate a wallet key from a serialized CBitcoinSecret. + * @param {Base58String} secret + * @returns {WalletKey} + */ + +WalletKey.fromSecret = function fromSecret(data) { + return new WalletKey().fromSecret(data); +}; + +/** + * Convert an WalletKey to a more json-friendly object. + * @returns {Object} + */ + +WalletKey.prototype.toJSON = function toJSON() { + return { + network: this.network.type, + witness: this.witness, + nested: this.nested, + publicKey: this.publicKey.toString('hex'), + script: this.script ? this.script.toRaw().toString('hex') : null, + program: this.program ? this.program.toRaw().toString('hex') : null, + type: constants.scriptTypesByVal[this.type].toLowerCase(), + wid: this.wid, + id: this.id, + name: this.name, + account: this.account, + branch: this.branch, + index: this.index, + address: this.getAddress('base58') + }; +}; + +/** + * Instantiate an WalletKey from a jsonified transaction object. + * @param {Object} json - The jsonified transaction object. + * @returns {WalletKey} + */ + +WalletKey.fromJSON = function fromJSON(json) { + return new WalletKey().fromJSON(json); +}; + +/** + * Instantiate a wallet key from serialized data. + * @param {Buffer} data + * @returns {WalletKey} + */ + +WalletKey.fromRaw = function fromRaw(data) { + return new WalletKey().fromRaw(data); +}; + +/** + * Inject properties from hd key. + * @private + * @param {Account} account + * @param {HDPrivateKey|HDPublicKey} key + * @param {Number} branch + * @param {Number} index + * @returns {WalletKey} + */ + +WalletKey.prototype.fromHD = function fromHD(account, key, branch, index) { + this.keyType = Path.types.HD; + this.id = account.id; + this.wid = account.wid; + this.name = account.name; + this.account = account.accountIndex; + this.branch = branch; + this.index = index; + this.witness = account.witness; + this.nested = branch === 2; + + if (key.privateKey) + return this.fromPrivate(key.privateKey, account.network); + + return this.fromPublic(key.publicKey, account.network); +}; + +/** + * Instantiate a wallet key from hd key. + * @param {Account} account + * @param {HDPrivateKey|HDPublicKey} key + * @param {Number} branch + * @param {Number} index + * @returns {WalletKey} + */ + +WalletKey.fromHD = function fromHD(account, key, branch, index) { + return new WalletKey().fromHD(account, key, branch, index); +}; + +/** + * Inject properties from imported data. + * @private + * @param {Account} account + * @param {Buffer} data + * @returns {WalletKey} + */ + +WalletKey.prototype.fromImport = function fromImport(account, data) { + this.keyType = Path.types.KEY; + this.id = account.id; + this.wid = account.wid; + this.name = account.name; + this.account = account.accountIndex; + this.witness = account.witness; + return this.fromRaw(data, account.network); +}; + +/** + * Instantiate a wallet key from imported data. + * @param {Account} account + * @param {Buffer} data + * @returns {WalletKey} + */ + +WalletKey.fromImport = function fromImport(account, data) { + return new WalletKey().fromImport(account, data); +}; + +/** + * Inject properties from key. + * @private + * @param {Account} account + * @param {KeyRing} ring + * @returns {WalletKey} + */ + +WalletKey.prototype.fromRing = function fromRing(account, ring) { + this.keyType = Path.types.KEY; + this.id = account.id; + this.wid = account.wid; + this.name = account.name; + this.account = account.accountIndex; + this.witness = account.witness; + return this.fromOptions(ring, ring.network); +}; + +/** + * Instantiate a wallet key from regular key. + * @param {Account} account + * @param {KeyRing} ring + * @returns {WalletKey} + */ + +WalletKey.fromRing = function fromRing(account, ring) { + return new WalletKey().fromRing(account, ring); +}; + +/** + * Convert wallet key to a path. + * @returns {Path} + */ + +WalletKey.prototype.toPath = function toPath() { + var path = new Path(); + + path.id = this.id; + path.wid = this.wid; + path.name = this.name; + path.account = this.account; + + switch (this.keyType) { + case Path.types.HD: + path.branch = this.branch; + path.index = this.index; + break; + case Path.types.KEY: + path.data = this.toRaw(); + break; + } + + path.keyType = this.keyType; + + path.version = this.getVersion(); + path.type = this.getType(); + path.hash = this.getHash('hex'); + + return path; +}; + +/** + * Test whether an object is a WalletKey. + * @param {Object} obj + * @returns {Boolean} + */ + +WalletKey.isWalletKey = function isWalletKey(obj) { + return obj + && Buffer.isBuffer(obj.publicKey) + && typeof obj.index === 'number' + && typeof obj.toPath === 'function'; +}; + +/* + * Expose + */ + +module.exports = WalletKey; diff --git a/lib/workers/framer.js b/lib/workers/framer.js new file mode 100644 index 00000000..523108b2 --- /dev/null +++ b/lib/workers/framer.js @@ -0,0 +1,146 @@ +/*! + * workers.js - worker processes for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var bn = require('bn.js'); +var utils = require('../utils/utils'); +var assert = require('assert'); +var BufferWriter = require('../utils/writer'); +var Block = require('../primitives/block'); +var MTX = require('../primitives/mtx'); +var TX = require('../primitives/tx'); +var Coin = require('../primitives/coin'); +var KeyRing = require('../primitives/keyring'); +var Script = require('../script/script'); +var Witness = require('../script/witness'); +var HD = require('../hd/hd'); +var MinerBlock = require('../miner/minerblock'); + +/** + * Framer + * @constructor + */ + +function Framer() { + if (!(this instanceof Framer)) + return new Framer(); + + EventEmitter.call(this); +} + +utils.inherits(Framer, EventEmitter); + +Framer.prototype.packet = function packet(job, cmd, items) { + var payload = this.body(items); + var packet = new Buffer(25 + payload.length + 1); + + assert(cmd.length < 12); + assert(payload.length <= 0xffffffff); + + packet.writeUInt32LE(0xdeadbeef, 0, true); + packet.writeUInt32LE(job, 4, true); + packet.writeUInt8(cmd.length, 8, true); + packet.write(cmd, 9, 'ascii'); + packet.writeUInt32LE(payload.length, 21, true); + payload.copy(packet, 25); + packet[packet.length - 1] = 0x0a; + + return packet; +}; + +Framer.prototype.body = function body(items) { + return Framer.item(items); +}; + +Framer.item = function _item(item, writer) { + var p = BufferWriter(writer); + var i, keys; + + switch (typeof item) { + case 'string': + p.writeU8(1); + p.writeVarString(item, 'utf8'); + break; + case 'number': + p.writeU8(2); + p.write32(item); + break; + case 'boolean': + p.writeU8(3); + p.writeU8(item ? 1 : 0); + break; + case 'object': + case 'undefined': + if (item == null) { + p.writeU8(0); + } else { + if (item instanceof Block) { + p.writeU8(40); + item.toRaw(p); + } else if (item instanceof MTX) { + p.writeU8(46); + item.toExtended(true, p); + } else if (item instanceof TX) { + p.writeU8(41); + item.toExtended(true, p); + } else if (item instanceof Coin) { + p.writeU8(42); + item.toExtended(p); + } else if (item instanceof MinerBlock) { + p.writeU8(45); + item.toRaw(p); + } else if (item instanceof KeyRing) { + p.writeU8(47); + item.toRaw(p); + } else if (HD.isHD(item)) { + p.writeU8(48); + p.writeBytes(item.toRaw()); + } else if (item instanceof Script) { + p.writeU8(49); + p.writeVarBytes(item.toRaw()); + } else if (item instanceof Witness) { + p.writeU8(50); + item.toRaw(p); + } else if (bn.isBN(item)) { + p.writeU8(10); + p.writeVarBytes(item.toArrayLike(Buffer)); + } else if (Buffer.isBuffer(item)) { + p.writeU8(4); + p.writeVarBytes(item); + } else if (Array.isArray(item)) { + p.writeU8(5); + p.writeVarint(item.length); + for (i = 0; i < item.length; i++) + Framer.item(item[i], p); + } else { + keys = Object.keys(item); + p.writeU8(6); + p.writeVarint(keys.length); + for (i = 0; i < keys.length; i++) { + p.writeVarString(keys[i], 'utf8'); + Framer.item(item[keys[i]], p); + } + } + } + break; + default: + throw new Error('Bad type.'); + } + + if (!writer) + p = p.render(); + + return p; +}; + +/* + * Expose + */ + +module.exports = Framer; diff --git a/lib/workers/index.js b/lib/workers/index.js new file mode 100644 index 00000000..fffe0191 --- /dev/null +++ b/lib/workers/index.js @@ -0,0 +1,7 @@ +'use strict'; + +exports.jobs = require('./jobs'); +exports.Worker = require('./worker'); +exports.Workers = require('./workers'); +exports.Parser = require('./parser'); +exports.Framer = require('./framer'); diff --git a/lib/workers/jobs.js b/lib/workers/jobs.js index 0733d320..8d14eaf6 100644 --- a/lib/workers/jobs.js +++ b/lib/workers/jobs.js @@ -6,7 +6,8 @@ 'use strict'; -var bcoin = require('../env'); +var ec = require('../crypto/ec'); +var scrypt = require('../crypto/scrypt'); /** * Jobs to execute within the worker. @@ -16,6 +17,13 @@ var bcoin = require('../env'); var jobs = exports; +/** + * Master process. + * @type {Master} + */ + +jobs.master = null; + /** * Execute tx.verify() on worker. * @see TX#verify @@ -29,11 +37,24 @@ jobs.verify = function verify(tx, flags) { }; /** - * Execute Wallet.sign() on worker. - * @see Wallet.sign - * @param {KeyRing[]} rings - * @param {HDPrivateKey} master + * Execute tx.verifyInput() on worker. + * @see TX#verifyInput + * @param {TX} tx + * @param {Number} index + * @param {VerifyFlags} flags + * @returns {Boolean} + */ + +jobs.verifyInput = function verifyInput(tx, index, flags) { + return tx.verifyInput(index, flags); +}; + +/** + * Execute tx.sign() on worker. + * @see MTX#sign * @param {MTX} tx + * @param {KeyRing[]} ring + * @param {SighashType} type */ jobs.sign = function sign(tx, ring, type) { @@ -49,6 +70,50 @@ jobs.sign = function sign(tx, ring, type) { return [sigs, total]; }; +/** + * Execute tx.signInput() on worker. + * @see MTX#signInput + * @param {MTX} tx + * @param {Number} index + * @param {Buffer} key + * @param {SighashType} type + */ + +jobs.signInput = function signInput(tx, index, key, type) { + var result = tx.signInput(tx, index, key, type); + var input = tx.inputs[index]; + + if (!result) + return null; + + return [input.script, input.witness]; +}; + +/** + * Execute ec.verify() on worker. + * @see ec.verify + * @param {TX} tx + * @param {VerifyFlags} flags + * @returns {Boolean} + */ + +jobs.ecVerify = function ecVerify(msg, sig, key) { + return ec.verify(msg, sig, key); +}; + +/** + * Execute ec.sign() on worker. + * @see ec.sign + * @param {TX} tx + * @param {Number} index + * @param {VerifyFlags} flags + * @returns {Boolean} + */ + +jobs.ecSign = function ecSign(msg, key) { + return ec.sign(msg, key); +}; + /** * Mine a block on worker. * @param {Object} attempt - Naked {@link MinerBlock}. @@ -56,9 +121,11 @@ jobs.sign = function sign(tx, ring, type) { */ jobs.mine = function mine(attempt) { - attempt.on('status', function(stat) { - bcoin.master.sendEvent('status', stat); - }); + if (jobs.master) { + attempt.on('status', function(status) { + jobs.master.sendEvent('status', status); + }); + } return attempt.mineSync(); }; @@ -74,7 +141,6 @@ jobs.mine = function mine(attempt) { * @returns {Buffer} */ -jobs.scrypt = function scrypt(passwd, salt, N, r, p, len) { - var scrypt = require('../crypto/scrypt'); +jobs.scrypt = function _scrypt(passwd, salt, N, r, p, len) { return scrypt(passwd, salt, N >>> 0, r >>> 0, p >>> 0, len); }; diff --git a/lib/workers/parser.js b/lib/workers/parser.js new file mode 100644 index 00000000..cdea2eed --- /dev/null +++ b/lib/workers/parser.js @@ -0,0 +1,225 @@ +/*! + * workers.js - worker processes for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * Copyright (c) 2014-2016, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +var EventEmitter = require('events').EventEmitter; +var bn = require('bn.js'); +var utils = require('../utils/utils'); +var assert = require('assert'); +var BufferReader = require('../utils/reader'); +var Block = require('../primitives/block'); +var MTX = require('../primitives/mtx'); +var TX = require('../primitives/tx'); +var Coin = require('../primitives/coin'); +var KeyRing = require('../primitives/keyring'); +var Script = require('../script/script'); +var Witness = require('../script/witness'); +var HD = require('../hd/hd'); +var MinerBlock = require('../miner/minerblock'); + +/** + * Parser + * @constructor + */ + +function Parser() { + if (!(this instanceof Parser)) + return new Parser(); + + EventEmitter.call(this); + + this.waiting = 25; + this.packet = null; + this.pending = []; + this.total = 0; +} + +utils.inherits(Parser, EventEmitter); + +Parser.prototype.feed = function feed(data) { + var chunk; + + this.total += data.length; + this.pending.push(data); + + while (this.total >= this.waiting) { + chunk = this.read(this.waiting); + this.parse(chunk); + } +}; + +Parser.prototype.read = function read(size) { + var pending, chunk, off, len; + + assert(this.total >= size, 'Reading too much.'); + + if (size === 0) + return new Buffer(0); + + pending = this.pending[0]; + + if (pending.length > size) { + chunk = pending.slice(0, size); + this.pending[0] = pending.slice(size); + this.total -= chunk.length; + return chunk; + } + + if (pending.length === size) { + chunk = this.pending.shift(); + this.total -= chunk.length; + return chunk; + } + + chunk = new Buffer(size); + off = 0; + len = 0; + + while (off < chunk.length) { + pending = this.pending[0]; + len = pending.copy(chunk, off); + if (len === pending.length) + this.pending.shift(); + else + this.pending[0] = pending.slice(len); + off += len; + } + + assert.equal(off, chunk.length); + + this.total -= chunk.length; + + return chunk; +}; + +Parser.prototype.parse = function parse(data) { + var packet = this.packet; + + if (!packet) { + try { + packet = this.parseHeader(data); + } catch (e) { + this.emit('error', e); + return; + } + + this.packet = packet; + this.waiting = packet.size + 1; + + return; + } + + this.waiting = 25; + this.packet = null; + + try { + packet.items = this.parseBody(data); + } catch (e) { + this.emit('error', e); + return; + } + + if (data[data.length - 1] !== 0x0a) { + this.emit('error', new Error('No trailing newline.')); + return; + } + + this.emit('packet', packet); +}; + +Parser.prototype.parseHeader = function parseHeader(data) { + var magic, job, len, cmd, size; + + magic = data.readUInt32LE(0, true); + + if (magic !== 0xdeadbeef) + throw new Error('Bad magic number: ' + magic.toString(16)); + + job = data.readUInt32LE(4, true); + + len = data[8]; + cmd = data.toString('ascii', 9, 9 + len); + + size = data.readUInt32LE(21, true); + + return new Packet(job, cmd, size); +}; + +Parser.prototype.parseBody = function parseBody(data) { + return Parser.parseItem(data); +}; + +Parser.parseItem = function parseItem(data) { + var p = BufferReader(data); + var i, count, items; + + switch (p.readU8()) { + case 0: + return null; + case 1: + return p.readVarString('utf8'); + case 2: + return p.read32(); + case 3: + return p.readU8() === 1; + case 4: + return p.readVarBytes(); + case 5: + items = []; + count = p.readVarint(); + for (i = 0; i < count; i++) + items.push(Parser.parseItem(p)); + return items; + case 6: + items = {}; + count = p.readVarint(); + for (i = 0; i < count; i++) + items[p.readVarString('utf8')] = Parser.parseItem(p); + return items; + case 10: + return new bn(p.readVarBytes()); + case 40: + return Block.fromRaw(p); + case 41: + return TX.fromExtended(p, true); + case 42: + return Coin.fromExtended(p); + case 45: + return MinerBlock.fromRaw(p); + case 46: + return MTX.fromExtended(p, true); + case 47: + return KeyRing.fromRaw(p); + case 48: + return HD.fromRaw(p.readBytes(82)); + case 49: + return Script.fromRaw(p.readVarBytes()); + case 50: + return Witness.fromRaw(p); + default: + throw new Error('Bad type.'); + } +}; + +/** + * Packet + * @constructor + */ + +function Packet(job, cmd, size) { + this.job = job; + this.cmd = cmd; + this.size = size; + this.items = null; +} + +/* + * Expose + */ + +module.exports = Parser; diff --git a/lib/workers/workers.js b/lib/workers/workers.js index a328ba3a..05c1974a 100644 --- a/lib/workers/workers.js +++ b/lib/workers/workers.js @@ -7,15 +7,16 @@ 'use strict'; -var bcoin = require('../env'); +module.exports = Workers; + var EventEmitter = require('events').EventEmitter; -var bn = require('bn.js'); var utils = require('../utils/utils'); +var co = require('../utils/co'); var global = utils.global; -var assert = utils.assert; -var BufferWriter = require('../utils/writer'); -var BufferReader = require('../utils/reader'); +var Network = require('../protocol/network'); var jobs = require('./jobs'); +var Parser = require('./parser'); +var Framer = require('./framer'); /** * A worker pool. @@ -212,71 +213,132 @@ Workers.prototype.destroy = function destroy() { * Call a method for a worker to execute. * @param {String} method - Method name. * @param {Array} args - Arguments. - * @param {Function} callback - Returns whatever + * @returns {Promise} * the worker method specifies. */ -Workers.prototype.execute = function execute(method, args, timeout, callback) { - var child; +Workers.prototype.execute = function execute(method, args, timeout) { + var result, child; + + if (!Workers.enabled) { + try { + result = jobs[method].apply(jobs, args); + } catch (e) { + return Promise.reject(e); + } + return Promise.resolve(result); + } if (!timeout) timeout = this.timeout; child = this.alloc(); - child.execute(method, args, timeout, callback); - - return child; + return child.execute(method, args, timeout); }; /** * Execute the tx verification job (default timeout). * @param {TX} tx * @param {VerifyFlags} flags - * @param {Function} callback - Returns [Error, Boolean]. + * @returns {Promise} - Returns Boolean. */ -Workers.prototype.verify = function verify(tx, flags, callback) { - this.execute('verify', [tx, flags], -1, callback); +Workers.prototype.verify = function verify(tx, flags) { + return this.execute('verify', [tx, flags], -1); +}; + +/** + * Execute the tx input verification job (default timeout). + * @param {TX} tx + * @param {Number} index + * @param {VerifyFlags} flags + * @returns {Promise} - Returns Boolean. + */ + +Workers.prototype.verifyInput = function verifyInput(tx, index, flags) { + return this.execute('verifyInput', [tx, index, flags], -1); }; /** * Execute the tx signing job (default timeout). - * @param {KeyRing[]} rings - * @param {HDPrivateKey} master * @param {MTX} tx - * @param {Function} callback + * @param {KeyRing[]} ring + * @param {SighashType} type + * @returns {Promise} */ -Workers.prototype.sign = function sign(tx, ring, type, callback) { - var i, input, sig, sigs, total; +Workers.prototype.sign = co(function* sign(tx, ring, type) { + var i, result, input, sig, sigs, total; - this.execute('sign', [tx, ring, type], -1, function(err, result) { - if (err) - return callback(err); + result = yield this.execute('sign', [tx, ring, type], -1); - sigs = result[0]; - total = result[1]; + sigs = result[0]; + total = result[1]; - for (i = 0; i < sigs.length; i++) { - sig = sigs[i]; - input = tx.inputs[i]; - input.script = sig[0]; - input.witness = sig[1]; - } + for (i = 0; i < sigs.length; i++) { + sig = sigs[i]; + input = tx.inputs[i]; + input.script = sig[0]; + input.witness = sig[1]; + } - callback(null, total); - }); + return total; +}); + +/** + * Execute the tx input signing job (default timeout). + * @param {MTX} tx + * @param {Number} index + * @param {Buffer} key + * @param {SighashType} type + * @returns {Promise} + */ + +Workers.prototype.signInput = co(function* signInput(tx, index, key, type) { + var sig = yield this.execute('signInput', [tx, index, key, type], -1); + var input = tx.inputs[index]; + + if (!sig) + return false; + + input.script = sig[0]; + input.witness = sig[1]; + + return true; +}); + +/** + * Execute the ec verify job (no timeout). + * @param {Buffer} msg + * @param {Buffer} sig - DER formatted. + * @param {Buffer} key + * @returns {Promise} + */ + +Workers.prototype.ecVerify = function ecVerify(msg, sig, key) { + return this.execute('ecVerify', [msg, sig, key], -1); +}; + +/** + * Execute the ec signing job (no timeout). + * @param {Buffer} msg + * @param {Buffer} key + * @returns {Promise} + */ + +Workers.prototype.ecSign = function ecSign(msg, key) { + return this.execute('ecSign', [msg, key], -1); }; /** * Execute the mining job (no timeout). * @param {MinerBlock} attempt - * @param {Function} callback - Returns [Error, {@link MinerBlock}]. + * @returns {Promise} - Returns {@link MinerBlock}. */ -Workers.prototype.mine = function mine(attempt, callback) { - this.execute('mine', [attempt], -1, callback); +Workers.prototype.mine = function mine(attempt) { + return this.execute('mine', [attempt], -1); }; /** @@ -287,12 +349,12 @@ Workers.prototype.mine = function mine(attempt, callback) { * @param {Number} r * @param {Number} p * @param {Number} len - * @param {Function} callback + * @returns {Promise} * @returns {Buffer} */ -Workers.prototype.scrypt = function scrypt(passwd, salt, N, r, p, len, callback) { - this.execute('scrypt', [passwd, salt, N, r, p, len], -1, callback); +Workers.prototype.scrypt = function scrypt(passwd, salt, N, r, p, len) { + return this.execute('scrypt', [passwd, salt, N, r, p, len], -1); }; /** @@ -318,6 +380,8 @@ function Worker(id) { this._init(); } +utils.inherits(Worker, EventEmitter); + /** * Initialize worker. Bind to events. * @private @@ -328,7 +392,7 @@ Worker.prototype._init = function _init() { var penv, cp; penv = { - BCOIN_WORKER_NETWORK: bcoin.network.get().type + BCOIN_WORKER_NETWORK: Network.type }; if (utils.isBrowser) { @@ -519,14 +583,15 @@ Worker.prototype.destroy = function destroy() { /** * Call a method for a worker to execute. + * @private * @param {Number} job - Job ID. * @param {String} method - Method name. * @param {Array} args - Arguments. - * @param {Function} callback - Returns whatever + * @returns {Promise} * the worker method specifies. */ -Worker.prototype.execute = function execute(method, args, timeout, callback) { +Worker.prototype._execute = function _execute(method, args, timeout, callback) { var self = this; var job = this.uid; var event, timer; @@ -564,7 +629,21 @@ Worker.prototype.execute = function execute(method, args, timeout, callback) { this.send(job, method, args); }; -utils.inherits(Worker, EventEmitter); +/** + * Call a method for a worker to execute. + * @param {Number} job - Job ID. + * @param {String} method - Method name. + * @param {Array} args - Arguments. + * @returns {Promise} + * the worker method specifies. + */ + +Worker.prototype.execute = function execute(method, args, timeout) { + var self = this; + return new Promise(function(resolve, reject) { + self._execute(method, args, timeout, co.wrap(resolve, reject)); + }); +}; /** * Represents the master process. @@ -741,334 +820,11 @@ Master.listen = function listen(options) { return master.send(packet.job, 'response', [null, result]); }); - bcoin.master = master; + jobs.master = master; return master; }; -/** - * Framer - * @constructor - */ - -function Framer() { - if (!(this instanceof Framer)) - return new Framer(); - - EventEmitter.call(this); -} - -utils.inherits(Framer, EventEmitter); - -Framer.prototype.packet = function packet(job, cmd, items) { - var payload = this.body(items); - var packet = new Buffer(25 + payload.length + 1); - - assert(cmd.length < 12); - assert(payload.length <= 0xffffffff); - - packet.writeUInt32LE(0xdeadbeef, 0, true); - packet.writeUInt32LE(job, 4, true); - packet.writeUInt8(cmd.length, 8, true); - packet.write(cmd, 9, 'ascii'); - packet.writeUInt32LE(payload.length, 21, true); - payload.copy(packet, 25); - packet[packet.length - 1] = 0x0a; - - return packet; -}; - -Framer.prototype.body = function body(items) { - return Framer.item(items); -}; - -Framer.item = function _item(item, writer) { - var p = BufferWriter(writer); - var i, keys; - - switch (typeof item) { - case 'string': - p.writeU8(1); - p.writeVarString(item, 'utf8'); - break; - case 'number': - p.writeU8(2); - p.write32(item); - break; - case 'boolean': - p.writeU8(3); - p.writeU8(item ? 1 : 0); - break; - case 'object': - case 'undefined': - if (item == null) { - p.writeU8(0); - } else { - if (item instanceof bcoin.block) { - p.writeU8(40); - item.toRaw(p); - } else if (item instanceof bcoin.mtx) { - p.writeU8(46); - item.toExtended(true, p); - } else if (item instanceof bcoin.tx) { - p.writeU8(41); - item.toExtended(true, p); - } else if (item instanceof bcoin.coin) { - p.writeU8(42); - item.toExtended(p); - } else if (item instanceof bcoin.chainentry) { - p.writeU8(43); - item.toRaw(p); - } else if (item instanceof bcoin.mempoolentry) { - p.writeU8(44); - item.toRaw(p); - } else if (item instanceof bcoin.minerblock) { - p.writeU8(45); - item.toRaw(p); - } else if (item instanceof bcoin.keyring) { - p.writeU8(47); - item.toRaw(p); - } else if (bcoin.hd.isHD(item)) { - p.writeU8(48); - p.writeBytes(item.toRaw()); - } else if (item instanceof bcoin.script) { - p.writeU8(49); - p.writeVarBytes(item.toRaw()); - } else if (item instanceof bcoin.witness) { - p.writeU8(50); - item.toRaw(p); - } else if (bn.isBN(item)) { - p.writeU8(10); - p.writeVarBytes(item.toArrayLike(Buffer)); - } else if (Buffer.isBuffer(item)) { - p.writeU8(4); - p.writeVarBytes(item); - } else if (Array.isArray(item)) { - p.writeU8(5); - p.writeVarint(item.length); - for (i = 0; i < item.length; i++) - Framer.item(item[i], p); - } else { - keys = Object.keys(item); - p.writeU8(6); - p.writeVarint(keys.length); - for (i = 0; i < keys.length; i++) { - p.writeVarString(keys[i], 'utf8'); - Framer.item(item[keys[i]], p); - } - } - } - break; - default: - throw new Error('Bad type.'); - } - - if (!writer) - p = p.render(); - - return p; -}; - -/** - * Parser - * @constructor - */ - -function Parser() { - if (!(this instanceof Parser)) - return new Parser(); - - EventEmitter.call(this); - - this.waiting = 25; - this.packet = null; - this.pending = []; - this.total = 0; -} - -utils.inherits(Parser, EventEmitter); - -Parser.prototype.feed = function feed(data) { - var chunk; - - this.total += data.length; - this.pending.push(data); - - while (this.total >= this.waiting) { - chunk = this.read(this.waiting); - this.parse(chunk); - } -}; - -Parser.prototype.read = function read(size) { - var pending, chunk, off, len; - - assert(this.total >= size, 'Reading too much.'); - - if (size === 0) - return new Buffer(0); - - pending = this.pending[0]; - - if (pending.length > size) { - chunk = pending.slice(0, size); - this.pending[0] = pending.slice(size); - this.total -= chunk.length; - return chunk; - } - - if (pending.length === size) { - chunk = this.pending.shift(); - this.total -= chunk.length; - return chunk; - } - - chunk = new Buffer(size); - off = 0; - len = 0; - - while (off < chunk.length) { - pending = this.pending[0]; - len = pending.copy(chunk, off); - if (len === pending.length) - this.pending.shift(); - else - this.pending[0] = pending.slice(len); - off += len; - } - - assert.equal(off, chunk.length); - - this.total -= chunk.length; - - return chunk; -}; - -Parser.prototype.parse = function parse(data) { - var packet = this.packet; - - if (!packet) { - try { - packet = this.parseHeader(data); - } catch (e) { - this.emit('error', e); - return; - } - - this.packet = packet; - this.waiting = packet.size + 1; - - return; - } - - this.waiting = 25; - this.packet = null; - - try { - packet.items = this.parseBody(data); - } catch (e) { - this.emit('error', e); - return; - } - - if (data[data.length - 1] !== 0x0a) { - this.emit('error', new Error('No trailing newline.')); - return; - } - - this.emit('packet', packet); -}; - -Parser.prototype.parseHeader = function parseHeader(data) { - var magic, job, len, cmd, size; - - magic = data.readUInt32LE(0, true); - - if (magic !== 0xdeadbeef) - throw new Error('Bad magic number: ' + magic.toString(16)); - - job = data.readUInt32LE(4, true); - - len = data[8]; - cmd = data.toString('ascii', 9, 9 + len); - - size = data.readUInt32LE(21, true); - - return new Packet(job, cmd, size); -}; - -Parser.prototype.parseBody = function parseBody(data) { - return Parser.parseItem(data); -}; - -Parser.parseItem = function parseItem(data) { - var p = BufferReader(data); - var i, count, items; - - switch (p.readU8()) { - case 0: - return null; - case 1: - return p.readVarString('utf8'); - case 2: - return p.read32(); - case 3: - return p.readU8() === 1; - case 4: - return p.readVarBytes(); - case 5: - items = []; - count = p.readVarint(); - for (i = 0; i < count; i++) - items.push(Parser.parseItem(p)); - return items; - case 6: - items = {}; - count = p.readVarint(); - for (i = 0; i < count; i++) - items[p.readVarString('utf8')] = Parser.parseItem(p); - return items; - case 10: - return new bn(p.readVarBytes()); - case 40: - return bcoin.block.fromRaw(p); - case 41: - return bcoin.tx.fromExtended(p, true); - case 42: - return bcoin.coin.fromExtended(p); - case 43: - return bcoin.chainentry.fromRaw(null, p); - case 44: - return bcoin.mempoolentry.fromRaw(p); - case 45: - return bcoin.minerblock.fromRaw(p); - case 46: - return bcoin.mtx.fromExtended(p, true); - case 47: - return bcoin.keyring.fromRaw(p); - case 48: - return bcoin.hd.fromRaw(p.readBytes(82)); - case 49: - return bcoin.script.fromRaw(p.readVarBytes()); - case 50: - return bcoin.witness.fromRaw(p); - default: - throw new Error('Bad type.'); - } -}; - -/** - * Packet - * @constructor - */ - -function Packet(job, cmd, size) { - this.job = job; - this.cmd = cmd; - this.size = size; - this.items = null; -} - /* * Helpers */ @@ -1095,6 +851,35 @@ function fromError(err) { return [err.message, err.stack + '', err.type]; } +/* + * Default + */ + +Workers.pool = new Workers(); +Workers.enabled = false; + +Workers.set = function set(options) { + if (typeof options.useWorkers === 'boolean') + this.enabled = options.useWorkers; + + if (utils.isNumber(options.maxWorkers)) + this.pool.size = options.maxWorkers; + + if (utils.isNumber(options.workerTimeout)) + this.pool.timeout = options.workerTimeout; + + if (utils.isBrowser && this.enabled) { + this.enabled = typeof global.Worker === 'function' + || typeof global.postMessage === 'function'; + } +}; + +Workers.set({ + useWorkers: +process.env.BCOIN_USE_WORKERS === 1, + maxWorkers: +process.env.BCOIN_MAX_WORKERS, + workerTimeout: +process.env.BCOIN_WORKER_TIMEOUT +}); + /* * Expose */ diff --git a/migrate/chaindb0to1.js b/migrate/chaindb0to1.js index 8d5dd384..af612c47 100644 --- a/migrate/chaindb0to1.js +++ b/migrate/chaindb0to1.js @@ -1,9 +1,13 @@ var bcoin = require('../'); +var co = bcoin.co; var assert = require('assert'); var file = process.argv[2]; +var BufferWriter = require('../lib/utils/writer'); assert(typeof file === 'string', 'Please pass in a database path.'); +file = file.replace(/\.ldb\/?$/, ''); + var db = bcoin.ldb({ location: file, db: 'leveldb', @@ -22,91 +26,93 @@ function makeKey(data) { return key; } -function updateState(callback) { - var hash, batch, ver, p; +var checkVersion = co(function* checkVersion() { + var data, ver; + + console.log('Checking version.'); + + data = yield db.get('V'); + + if (!data) + return; + + ver = data.readUInt32LE(0, true); + + if (ver !== 0) + throw Error('DB is version ' + ver + '.'); +}); + +var updateState = co(function* updateState() { + var data, hash, batch, ver, p; console.log('Updating chain state.'); - db.get('R', function(err, data) { - if (err) - return callback(err); + data = yield db.get('R'); - if (!data || data.length < 32) - return callback(new Error('No chain state.')); + if (!data || data.length < 32) + throw new Error('No chain state.'); - hash = data.slice(0, 32); + hash = data.slice(0, 32); - p = new bcoin.writer(); - p.writeHash(hash); - p.writeU64(0); - p.writeU64(0); - p.writeU64(0); - p = p.render(); + p = new BufferWriter(); + p.writeHash(hash); + p.writeU64(0); + p.writeU64(0); + p.writeU64(0); + p = p.render(); - batch = db.batch(); + batch = db.batch(); - batch.put('R', p); + batch.put('R', p); - ver = new Buffer(4); - ver.writeUInt32LE(1, 0, true); - batch.put('V', ver); + ver = new Buffer(4); + ver.writeUInt32LE(1, 0, true); + batch.put('V', ver); - batch.write(function(err) { - if (err) - return callback(err); - console.log('Updated chain state.'); - callback(); - }); - }); -} + yield batch.write(); -function updateEndian(callback) { - var lo = new Buffer('4800000000', 'hex'); - var hi = new Buffer('48ffffffff', 'hex'); + console.log('Updated chain state.'); +}); + +var updateEndian = co(function* updateEndian() { var batch = db.batch(); var total = 0; + var iter, item; console.log('Updating endianness.'); console.log('Iterating...'); - db.iterate({ - gte: lo, - lte: hi, - values: true, - parse: function(key, value) { - batch.del(key); - batch.put(makeKey(key), value); - total++; - } - }, function(err) { - if (err) - throw err; - - console.log('Migrating %d items.', total); - - batch.write(function(err) { - if (err) - throw err; - console.log('Migrated endianness.'); - callback(); - }); + iter = db.iterator({ + gte: new Buffer('4800000000', 'hex'), + lte: new Buffer('48ffffffff', 'hex'), + values: true }); -} -db.open(function(err) { - if (err) - throw err; + for (;;) { + item = yield iter.next(); - console.log('Opened %s.', file); + if (!item) + break; - updateState(function(err) { - if (err) - throw err; - updateEndian(function(err) { - if (err) - throw err; - console.log('Migration complete.'); - process.exit(0); - }); - }); + batch.del(item.key); + batch.put(makeKey(item.key), item.value); + total++; + } + + console.log('Migrating %d items.', total); + + yield batch.write(); + + console.log('Migrated endianness.'); +}); + +co.spawn(function* () { + yield db.open(); + console.log('Opened %s.', file); + yield checkVersion(); + yield updateState(); + yield updateEndian(); +}).then(function() { + console.log('Migration complete.'); + process.exit(0); }); diff --git a/migrate/walletdb2to3.js b/migrate/walletdb2to3.js new file mode 100644 index 00000000..fe60a8b8 --- /dev/null +++ b/migrate/walletdb2to3.js @@ -0,0 +1,343 @@ +var bcoin = require('../'); +var walletdb = require('../lib/wallet/walletdb'); +var constants = require('../lib/protocol/constants'); +var Path = require('../lib/wallet/path'); +var MasterKey = require('../lib/wallet/masterkey'); +var Account = require('../lib/wallet/account'); +var Wallet = require('../lib/wallet/wallet'); +var layout = walletdb.layout; +var co = bcoin.co; +var assert = require('assert'); +var file = process.argv[2]; +var BufferReader = require('../lib/utils/reader'); +var BufferWriter = require('../lib/utils/writer'); +var db, batch; + +assert(typeof file === 'string', 'Please pass in a database path.'); + +file = file.replace(/\.ldb\/?$/, ''); + +db = bcoin.ldb({ + location: file, + db: 'leveldb', + compression: true, + cacheSize: 16 << 20, + writeBufferSize: 8 << 20, + createIfMissing: false, + bufferKeys: true +}); + +var updateVersion = co(function* updateVersion() { + var data, ver; + + console.log('Checking version.'); + + data = yield db.get('V'); + + if (!data) + return; + + ver = data.readUInt32LE(0, true); + + if (ver !== 2) + throw Error('DB is version ' + ver + '.'); + + console.log('Backing up DB.'); + + yield db.backup(process.env.HOME + '/walletdb-bak-' + Date.now() + '.ldb'); + + ver = new Buffer(4); + ver.writeUInt32LE(3, 0, true); + batch.put('V', ver); +}); + +var updatePathMap = co(function* updatePathMap() { + var total = 0; + var i, iter, item, oldPaths, oldPath; + var hash, path, keys, key, ring; + + iter = db.iterator({ + gte: layout.p(constants.NULL_HASH), + lte: layout.p(constants.HIGH_HASH), + values: true + }); + + console.log('Migrating path map.'); + + for (;;) { + item = yield iter.next(); + + if (!item) + break; + + total++; + hash = layout.pp(item.key); + oldPaths = parsePaths(item.value, hash); + keys = Object.keys(oldPaths); + + for (i = 0; i < keys.length; i++) { + keys[i] = +keys[i]; + key = keys[i]; + oldPath = oldPaths[key]; + path = new Path(oldPath); + if (path.data) { + if (path.encrypted) { + console.log( + 'Cannot migrate encrypted import: %s (%s)', + path.data.toString('hex'), + path.toAddress().toBase58()); + continue; + } + ring = keyFromRaw(path.data); + path.data = new bcoin.keyring(ring).toRaw(); + } + batch.put(layout.P(key, hash), path.toRaw()); + } + + batch.put(layout.p(hash), serializeWallets(keys)); + } + + console.log('Migrated %d paths.', total); +}); + +var updateAccounts = co(function* updateAccounts() { + var total = 0; + var iter, item, account, buf; + + iter = db.iterator({ + gte: layout.a(0, 0), + lte: layout.a(0xffffffff, 0xffffffff), + values: true + }); + + console.log('Migrating accounts.'); + + for (;;) { + item = yield iter.next(); + + if (!item) + break; + + total++; + account = accountFromRaw(item.value, item.key); + account = new Account({ network: account.network, options: {} }, account); + batch.put(layout.a(account.wid, account.accountIndex), account.toRaw()); + + if (account._old) { + batch.del(layout.i(account.wid, account._old)); + buf = new Buffer(4); + buf.writeUInt32LE(account.accountIndex, 0, true); + batch.put(layout.i(account.wid, account.name), buf); + } + } + + console.log('Migrated %d accounts.', total); +}); + +var updateWallets = co(function* updateWallets() { + var total = 0; + var iter, item, wallet, buf; + + iter = db.iterator({ + gte: layout.w(0), + lte: layout.w(0xffffffff), + values: true + }); + + console.log('Migrating wallets.'); + + for (;;) { + item = yield iter.next(); + + if (!item) + break; + + total++; + wallet = walletFromRaw(item.value); + wallet = new Wallet({ network: wallet.network }, wallet); + batch.put(layout.w(wallet.wid), wallet.toRaw()); + + if (wallet._old) { + batch.del(layout.l(wallet._old)); + buf = new Buffer(4); + buf.writeUInt32LE(wallet.wid, 0, true); + batch.put(layout.l(wallet.id), buf); + } + } + + console.log('Migrated %d wallets.', total); +}); + +function pathFromRaw(data) { + var path = {}; + var p = new BufferReader(data); + + path.wid = p.readU32(); + path.name = p.readVarString('utf8'); + path.account = p.readU32(); + + switch (p.readU8()) { + case 0: + path.keyType = 0; + path.branch = p.readU32(); + path.index = p.readU32(); + if (p.readU8() === 1) + assert(false, 'Cannot migrate custom redeem script.'); + break; + case 1: + path.keyType = 1; + path.encrypted = p.readU8() === 1; + path.data = p.readVarBytes(); + path.branch = -1; + path.index = -1; + break; + default: + assert(false); + break; + } + + path.version = p.read8(); + path.type = p.readU8(); + + return path; +} + +function parsePaths(data, hash) { + var p = new BufferReader(data); + var out = {}; + var path; + + while (p.left()) { + path = pathFromRaw(p); + out[path.wid] = path; + if (hash) + path.hash = hash; + } + + return out; +} + +function serializeWallets(wallets) { + var p = new BufferWriter(); + var i, wid; + + for (i = 0; i < wallets.length; i++) { + wid = wallets[i]; + p.writeU32(wid); + } + + return p.render(); +} + +function readAccountKey(key) { + return { + wid: key.readUInt32BE(1, true), + index: key.readUInt32BE(5, true) + }; +} + +function accountFromRaw(data, dbkey) { + var account = {}; + var p = new BufferReader(data); + var i, count, key, name; + + dbkey = readAccountKey(dbkey); + account.wid = dbkey.wid; + account.id = 'doesntmatter'; + account.network = bcoin.network.fromMagic(p.readU32()); + account.name = p.readVarString('utf8'); + account.initialized = p.readU8() === 1; + account.type = p.readU8(); + account.m = p.readU8(); + account.n = p.readU8(); + account.witness = p.readU8() === 1; + account.accountIndex = p.readU32(); + account.receiveDepth = p.readU32(); + account.changeDepth = p.readU32(); + account.accountKey = bcoin.hd.fromRaw(p.readBytes(82)); + account.keys = []; + account.watchOnly = false; + account.nestedDepth = 0; + + name = account.name.replace(/[^\-\._0-9A-Za-z]+/g, ''); + + if (name !== account.name) { + console.log('Account name changed: %s -> %s.', account.name, name); + account._old = account.name; + account.name = name; + } + + count = p.readU8(); + + for (i = 0; i < count; i++) { + key = bcoin.hd.fromRaw(p.readBytes(82)); + account.keys.push(key); + } + + return account; +} + +function walletFromRaw(data) { + var wallet = {}; + var p = new BufferReader(data); + var id; + + wallet.network = bcoin.network.fromMagic(p.readU32()); + wallet.wid = p.readU32(); + wallet.id = p.readVarString('utf8'); + wallet.initialized = p.readU8() === 1; + wallet.accountDepth = p.readU32(); + wallet.token = p.readBytes(32); + wallet.tokenDepth = p.readU32(); + wallet.master = MasterKey.fromRaw(p.readVarBytes()); + wallet.watchOnly = false; + + id = wallet.id.replace(/[^\-\._0-9A-Za-z]+/g, ''); + + if (id !== wallet.id) { + console.log('Wallet ID changed: %s -> %s.', wallet.id, id); + wallet._old = wallet.id; + wallet.id = id; + } + + return wallet; +} + +function keyFromRaw(data, network) { + var ring = {}; + var p = new BufferReader(data); + var key, script; + + ring.network = bcoin.network.get(network); + ring.witness = p.readU8() === 1; + + key = p.readVarBytes(); + + if (key.length === 32) { + ring.privateKey = key; + ring.publicKey = bcoin.ec.publicKeyCreate(key, true); + } else { + ring.publicKey = key; + } + + script = p.readVarBytes(); + + if (script.length > 0) + ring.script = bcoin.script.fromRaw(script); + + return ring; +} + +co.spawn(function* () { + yield db.open(); + batch = db.batch(); + console.log('Opened %s.', file); + yield updateVersion(); + yield updatePathMap(); + yield updateAccounts(); + yield updateWallets(); + yield batch.write(); +}).then(function() { + console.log('Migration complete.'); + process.exit(0); +}); diff --git a/package.json b/package.json index 65c2f1cb..33d36e31 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "test": "mocha --reporter spec test/*-test.js", "browserify": "browserify --im -o browser/bcoin.js lib/bcoin.js", "uglify": "uglifyjs -m -o browser/bcoin.min.js browser/bcoin.js", - "clean": "rm browser/bcoin.js browser/bcoin.min.js" + "clean": "rm browser/bcoin.js browser/bcoin.min.js", + "hint": "jshint lib/ || exit 0" }, "repository": "git://github.com/bcoin-org/bcoin.git", "keywords": [ @@ -33,20 +34,25 @@ }, "homepage": "https://github.com/bcoin-org/bcoin", "engines": { - "node": ">= 0.10.0" + "node": ">= 0.11.0" }, "dependencies": { "bn.js": "4.11.6", - "elliptic": "6.3.1" + "elliptic": "6.3.2" }, "optionalDependencies": { "bcoin-native": "0.0.6", - "leveldown": "1.5.0", + "leveldown": "git://github.com/Level/leveldown.git#leveldb-1.19", "secp256k1": "3.2.0", "socket.io": "1.4.8", "socket.io-client": "1.4.8" }, "devDependencies": { + "babelify": "7.3.0", + "babel-preset-es2015": "6.14.0", + "babel-polyfill": "6.13.0", + "babel-plugin-transform-runtime": "6.12.0", + "babel-plugin-transform-regenerator": "6.14.0", "browserify": "13.1.0", "hash.js": "1.0.3", "jsdoc": "3.4.0", @@ -59,7 +65,6 @@ "./lib/http/client": "./browser/empty.js", "./lib/http/request": "./browser/empty.js", "./lib/http/rpcclient": "./browser/empty.js", - "./lib/http/rpc": "./browser/empty.js", "./lib/http/server": "./browser/empty.js", "./lib/http/wallet": "./browser/empty.js", "fs": "./browser/empty.js", @@ -69,5 +74,8 @@ "net": "./browser/empty.js", "bcoin-native": "./browser/empty.js", "secp256k1": "./browser/empty.js" + }, + "browserify": { + "transform": ["./browser/transform.js", "babelify"] } } diff --git a/test/bip70-test.js b/test/bip70-test.js index 1737c014..0af9ec60 100644 --- a/test/bip70-test.js +++ b/test/bip70-test.js @@ -8,8 +8,8 @@ var constants = bcoin.constants; var network = bcoin.networks; var assert = require('assert'); var tests = require('./data/bip70.json'); -var bip70 = require('../lib/bip70/bip70'); -var x509 = require('../lib/bip70/x509'); +var bip70 = require('../lib/bip70'); +var x509 = bip70.x509; tests.valid = new Buffer(tests.valid, 'hex'); tests.invalid = new Buffer(tests.invalid, 'hex'); diff --git a/test/bloom-test.js b/test/bloom-test.js index 679862b8..b5136ba6 100644 --- a/test/bloom-test.js +++ b/test/bloom-test.js @@ -89,7 +89,7 @@ describe('Bloom', function() { }); it('should handle 1m ops with rolling filter', function() { - var filter = new bcoin.bloom.rolling(210000, 0.00001); + var filter = new bcoin.bloom.Rolling(210000, 0.00001); filter.tweak = 0xdeadbeef; // ~1m operations for (var i = 0; i < 1000; i++) { @@ -105,7 +105,7 @@ describe('Bloom', function() { }); it('should handle rolling generations', function() { - var filter = new bcoin.bloom.rolling(50, 0.00001); + var filter = new bcoin.bloom.Rolling(50, 0.00001); filter.tweak = 0xdeadbeee; for (var i = 0; i < 25; i++) { var str = 'foobar' + i; diff --git a/test/chain-test.js b/test/chain-test.js index c01fa0de..440039ac 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -7,12 +7,12 @@ var utils = bcoin.utils; var crypto = require('../lib/crypto/crypto'); var assert = require('assert'); var opcodes = constants.opcodes; - -constants.tx.COINBASE_MATURITY = 0; +var co = require('../lib/utils/co'); +var cob = co.cob; describe('Chain', function() { var chain, wallet, node, miner, walletdb; - var competingTip, oldTip, tip1, tip2, cb1, cb2; + var tip1, tip2, cb1, cb2; this.timeout(5000); @@ -22,230 +22,233 @@ describe('Chain', function() { miner = node.miner; node.on('error', function() {}); - function mineBlock(tip, tx, callback) { - miner.createBlock(tip, function(err, attempt) { - assert.ifError(err); - if (tx) { - var redeemer = bcoin.mtx(); - redeemer.addOutput({ - address: wallet.receiveAddress.getAddress(), - value: utils.satoshi('25.0') - }); - redeemer.addOutput({ - address: wallet.changeAddress.getAddress(), - value: utils.satoshi('5.0') - }); - redeemer.addInput(tx, 0); - redeemer.setLocktime(chain.height); - return wallet.sign(redeemer, function(err) { - assert.ifError(err); - attempt.addTX(redeemer.toTX()); - callback(null, attempt.mineSync()); - }); - } - callback(null, attempt.mineSync()); + var mineBlock = co(function* mineBlock(tip, tx) { + var attempt = yield miner.createBlock(tip); + var redeemer; + + if (!tx) + return attempt.mineSync(); + + redeemer = bcoin.mtx(); + + redeemer.addOutput({ + address: wallet.receive.getAddress(), + value: 25 * 1e8 }); - } + + redeemer.addOutput({ + address: wallet.change.getAddress(), + value: 5 * 1e8 + }); + + redeemer.addInput(tx, 0); + + redeemer.setLocktime(chain.height); + + yield wallet.sign(redeemer); + + attempt.addTX(redeemer.toTX()); + + return attempt.mineSync(); + }); function deleteCoins(tx) { + var i; + if (tx.txs) { deleteCoins(tx.txs); return; } + if (Array.isArray(tx)) { - tx.forEach(deleteCoins); + for (i = 0; i < tx.length; i++) + deleteCoins(tx[i]); return; } - tx.inputs.forEach(function(input) { - input.coin = null; - }); + + for (i = 0; i < tx.inputs.length; i++) + tx.inputs[i].coin = null; } - it('should open chain and miner', function(cb) { + it('should open chain and miner', cob(function* () { miner.mempool = null; - node.open(cb); - }); + constants.tx.COINBASE_MATURITY = 0; + yield node.open(); + })); - it('should open walletdb', function(cb) { - walletdb.create({}, function(err, w) { - assert.ifError(err); - wallet = w; - miner.address = wallet.getAddress(); - cb(); - }); - }); + it('should open walletdb', cob(function* () { + wallet = yield walletdb.create(); + miner.address = wallet.getAddress(); + })); - it('should mine a block', function(cb) { - miner.mineBlock(function(err, block) { - assert.ifError(err); - assert(block); - cb(); - }); - }); + it('should mine a block', cob(function* () { + var block = yield miner.mineBlock(); + assert(block); + })); - it('should mine competing chains', function(cb) { - utils.forRangeSerial(0, 10, function(i, next) { - mineBlock(tip1, cb1, function(err, block1) { - assert.ifError(err); - cb1 = block1.txs[0]; - mineBlock(tip2, cb2, function(err, block2) { - assert.ifError(err); - cb2 = block2.txs[0]; - deleteCoins(block1); - chain.add(block1, function(err) { - assert.ifError(err); - deleteCoins(block2); - chain.add(block2, function(err) { - assert.ifError(err); - assert(chain.tip.hash === block1.hash('hex')); - competingTip = block2.hash('hex'); - chain.db.get(block1.hash('hex'), function(err, entry1) { - assert.ifError(err); - chain.db.get(block2.hash('hex'), function(err, entry2) { - assert.ifError(err); - assert(entry1); - assert(entry2); - tip1 = entry1; - tip2 = entry2; - chain.db.isMainChain(block2.hash('hex'), function(err, result) { - assert.ifError(err); - assert(!result); - next(); - }); - }); - }); - }); - }); - }); - }); - }, cb); - }); + it('should mine competing chains', cob(function* () { + var i, block1, block2; + + for (i = 0; i < 10; i++) { + block1 = yield mineBlock(tip1, cb1); + cb1 = block1.txs[0]; + + block2 = yield mineBlock(tip2, cb2); + cb2 = block2.txs[0]; + + deleteCoins(block1); + yield chain.add(block1); + + deleteCoins(block2); + yield chain.add(block2); + + assert(chain.tip.hash === block1.hash('hex')); + + tip1 = yield chain.db.get(block1.hash('hex')); + tip2 = yield chain.db.get(block2.hash('hex')); + + assert(tip1); + assert(tip2); + + assert(!(yield tip2.isMainChain())); + } + })); + + it('should have correct balance', cob(function* () { + var balance; + + yield co.timeout(100); + + balance = yield wallet.getBalance(); + assert.equal(balance.unconfirmed, 0); + assert.equal(balance.confirmed, 500 * 1e8); + assert.equal(balance.total, 500 * 1e8); + })); + + it('should handle a reorg', cob(function* () { + var entry, block, forked; - it('should handle a reorg', function(cb) { assert.equal(walletdb.height, chain.height); assert.equal(chain.height, 10); - oldTip = chain.tip; - chain.db.get(competingTip, function(err, entry) { - assert.ifError(err); - assert(entry); - assert(chain.height === entry.height); - miner.mineBlock(entry, function(err, block) { - assert.ifError(err); - assert(block); - var forked = false; - chain.once('reorganize', function() { - forked = true; - }); - deleteCoins(block); - chain.add(block, function(err) { - assert.ifError(err); - assert(forked); - assert(chain.tip.hash === block.hash('hex')); - assert(chain.tip.chainwork.cmp(oldTip.chainwork) > 0); - cb(); - }); - }); + + entry = yield chain.db.get(tip2.hash); + assert(entry); + assert(chain.height === entry.height); + + block = yield miner.mineBlock(entry); + assert(block); + + forked = false; + chain.once('reorganize', function() { + forked = true; }); - }); - it('should check main chain', function(cb) { - chain.db.isMainChain(oldTip, function(err, result) { - assert.ifError(err); - assert(!result); - cb(); - }); - }); + deleteCoins(block); - it('should mine a block after a reorg', function(cb) { - mineBlock(null, cb2, function(err, block) { - assert.ifError(err); - deleteCoins(block); - chain.add(block, function(err) { - assert.ifError(err); - chain.db.get(block.hash('hex'), function(err, entry) { - assert.ifError(err); - assert(entry); - assert(chain.tip.hash === entry.hash); - chain.db.isMainChain(entry.hash, function(err, result) { - assert.ifError(err); - assert(result); - cb(); - }); - }); - }); - }); - }); + yield chain.add(block); - it('should fail to mine a block with coins on an alternate chain', function(cb) { - mineBlock(null, cb1, function(err, block) { - assert.ifError(err); - deleteCoins(block); - chain.add(block, function(err) { - assert(err); - cb(); - }); - }); - }); + assert(forked); + assert(chain.tip.hash === block.hash('hex')); + assert(chain.tip.chainwork.cmp(tip1.chainwork) > 0); + })); - it('should get coin', function(cb) { - mineBlock(null, null, function(err, block) { - assert.ifError(err); - chain.add(block, function(err) { - assert.ifError(err); - mineBlock(null, block.txs[0], function(err, block) { - assert.ifError(err); - chain.add(block, function(err) { - assert.ifError(err); - var tx = block.txs[1]; - var output = bcoin.coin.fromTX(tx, 1); - chain.db.getCoin(tx.hash('hex'), 1, function(err, coin) { - assert.ifError(err); - assert.deepEqual(coin.toRaw(), output.toRaw()); - cb(); - }); - }); - }); - }); - }); - }); + it('should have correct balance', cob(function* () { + var balance; - it('should get balance', function(cb) { - setTimeout(function() { - wallet.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(balance.unconfirmed, 23000000000); - assert.equal(balance.confirmed, 97000000000); - assert.equal(balance.total, 120000000000); - assert.equal(wallet.account.receiveDepth, 8); - assert.equal(wallet.account.changeDepth, 7); - assert.equal(walletdb.height, chain.height); - assert.equal(walletdb.tip, chain.tip.hash); - wallet.getHistory(function(err, txs) { - assert.ifError(err); - assert.equal(txs.length, 44); - cb(); - }); - }); - }, 100); - }); + yield co.timeout(100); - it('should rescan for transactions', function(cb) { + balance = yield wallet.getBalance(); + assert.equal(balance.unconfirmed, 500 * 1e8); + assert.equal(balance.confirmed, 550 * 1e8); + assert.equal(balance.total, 1050 * 1e8); + })); + + it('should check main chain', cob(function* () { + var result = yield tip1.isMainChain(); + assert(!result); + })); + + it('should mine a block after a reorg', cob(function* () { + var block, entry, result; + + block = yield mineBlock(null, cb2); + deleteCoins(block); + yield chain.add(block); + + entry = yield chain.db.get(block.hash('hex')); + assert(entry); + assert(chain.tip.hash === entry.hash); + + result = yield entry.isMainChain(); + assert(result); + })); + + it('should fail to mine a block with coins on an alternate chain', cob(function* () { + var block = yield mineBlock(null, cb1); + var err; + + deleteCoins(block); + + try { + yield chain.add(block); + } catch (e) { + err = e; + } + + assert(err); + assert.equal(err.reason, 'bad-txns-inputs-missingorspent'); + })); + + it('should get coin', cob(function* () { + var block, tx, output, coin; + + block = yield mineBlock(); + yield chain.add(block); + block = yield mineBlock(null, block.txs[0]); + yield chain.add(block); + + tx = block.txs[1]; + output = bcoin.coin.fromTX(tx, 1); + + coin = yield chain.db.getCoin(tx.hash('hex'), 1); + + assert.deepEqual(coin.toRaw(), output.toRaw()); + })); + + it('should get balance', cob(function* () { + var balance, txs; + + yield co.timeout(100); + + balance = yield wallet.getBalance(); + assert.equal(balance.unconfirmed, 500 * 1e8); + assert.equal(balance.confirmed, 700 * 1e8); + assert.equal(balance.total, 1200 * 1e8); + + assert(wallet.account.receiveDepth >= 8); + assert(wallet.account.changeDepth >= 7); + + assert.equal(walletdb.height, chain.height); + assert.equal(walletdb.tip, chain.tip.hash); + + txs = yield wallet.getHistory(); + assert.equal(txs.length, 44); + })); + + it('should rescan for transactions', cob(function* () { var total = 0; - walletdb.getAddressHashes(function(err, hashes) { - assert.ifError(err); - chain.db.scan(null, hashes, function(block, txs, next) { - total += txs.length; - next(); - }, function(err) { - assert.ifError(err); - assert.equal(total, 25); - cb(); - }); - }); - }); + var hashes = yield walletdb.getHashes(); - it('should cleanup', function(cb) { + yield chain.db.scan(null, hashes, function(block, txs) { + total += txs.length; + return Promise.resolve(null); + }); + + assert.equal(total, 25); + })); + + it('should cleanup', cob(function* () { constants.tx.COINBASE_MATURITY = 100; - node.close(cb); - }); + yield node.close(); + })); }); diff --git a/test/data/tx_invalid.json b/test/data/tx_invalid.json index 217a9cbe..f8baee05 100644 --- a/test/data/tx_invalid.json +++ b/test/data/tx_invalid.json @@ -200,41 +200,41 @@ ["CHECKSEQUENCEVERIFY tests"], ["By-height locks, with argument just beyond txin.nSequence"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "1 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "1 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000feff40000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["By-time locks, with argument just beyond txin.nSequence (but within numerical boundries)"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194305 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194305 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000feff40000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Argument missing"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Argument negative with by-blockheight txin.nSequence=0"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "-1 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "-1 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Argument negative with by-blocktime txin.nSequence=CTxIn::SEQUENCE_LOCKTIME_TYPE_FLAG"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "-1 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "-1 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Argument/tx height/time mismatch, both versions"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "65535 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "65535 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["6 byte non-minimally-encoded arguments are invalid even if their contents are valid"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0x06 0x000000000000 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0x06 0x000000000000 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Failure due to failing CHECKSEQUENCEVERIFY in scriptSig"], @@ -246,9 +246,9 @@ "0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Failure due to insufficient tx.nVersion (<2)"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 CHECKSEQUENCEVERIFY 1"]], "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 CHECKSEQUENCEVERIFY 1"]], "010000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Unknown witness program version (with DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM)"], @@ -310,5 +310,9 @@ [[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0x00 0x20 0x34b6c399093e06cf9f0f7f660a1abcfe78fcf7b576f43993208edd9518a0ae9b", 1000]], "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0001045102010100000000", "P2SH,WITNESS"], +["33 bytes push should be considered a witness scriptPubKey"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0x60 0x21 0xff25429251b5a84f452230a3c75fd886b7fc5a7865ce4a7bb7a9d7c5be6da3dbff", 1000]], +"010000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e803000000000000015100000000", "P2SH,WITNESS,DISCOURAGE_UPGRADABLE_WITNESS_PROGRAM"], + ["Make diffs cleaner by leaving a comment here without comma at the end"] ] diff --git a/test/data/tx_valid.json b/test/data/tx_valid.json index a681c630..1ea70135 100644 --- a/test/data/tx_valid.json +++ b/test/data/tx_valid.json @@ -236,77 +236,77 @@ ["CHECKSEQUENCEVERIFY tests"], ["By-height locks, with argument == 0 and == txin.nSequence"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "65535 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "65535 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "65535 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "65535 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["By-time locks, with argument == 0 and == txin.nSequence"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff40000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4259839 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Upper sequence with upper sequence is fine"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000800100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000feffffff0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Argument 2^31 with various nSequence"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483648 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Argument 2^32-1 with various nSequence"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4294967295 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Argument 3<<31 with various nSequence"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "6442450944 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "6442450944 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffbf7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "6442450944 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "6442450944 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffff7f0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "6442450944 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "6442450944 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff0100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["5 byte non-minimally-encoded operandss are valid"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0x05 0x0000000000 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0x05 0x0000000000 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["The argument can be calculated rather than created directly by a PUSHDATA"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194303 1ADD NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194303 1ADD CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 1SUB NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "4194304 1SUB CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000ffff00000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["An ADD producing a 5-byte result that sets CTxIn::SEQUENCE_LOCKTIME_DISABLE_FLAG"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483647 65536 NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483647 65536 CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], -[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483647 4259840 ADD NOP3 1"]], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "2147483647 4259840 ADD CHECKSEQUENCEVERIFY 1"]], "020000000100010000000000000000000000000000000000000000000000000000000000000000000000000040000100000000000000000000000000", "P2SH,CHECKSEQUENCEVERIFY"], ["Valid CHECKSEQUENCEVERIFY in scriptSig"], @@ -467,5 +467,25 @@ ["0000000000000000000000000000000000000000000000000000000000000100", 1, "0x21 0x03596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc71 CHECKSIG", 1001]], "01000000020001000000000000000000000000000000000000000000000000000000000000000000004847304402202a0b4b1294d70540235ae033d78e64b4897ec859c7b6f1b2b1d8a02e1d46006702201445e756d2254b0f1dfda9ab8e1e1bc26df9668077403204f32d16a49a36eb6983ffffffff00010000000000000000000000000000000000000000000000000000000000000100000049483045022100acb96cfdbda6dc94b489fd06f2d720983b5f350e31ba906cdbd800773e80b21c02200d74ea5bdf114212b4bbe9ed82c36d2e369e302dff57cb60d01c428f0bd3daab83ffffffff02e8030000000000000151e903000000000000015100000000", "P2SH,WITNESS"], +["BIP143 examples: details and private keys are available in BIP143"], +["BIP143 example: P2WSH with OP_CODESEPARATOR and out-of-range SIGHASH_SINGLE."], +[[["6eb316926b1c5d567cd6f5e6a84fec606fc53d7b474526d1fff3948020c93dfe", 0, "0x21 0x036d5c20fa14fb2f635474c1dc4ef5909d4568e5569b79fc94d3448486e14685f8 CHECKSIG", 156250000], +["f825690aee1b3dc247da796cacb12687a5e802429fd291cfd63e010f02cf1508", 0, "0x00 0x20 0x5d1b56b63d714eebe542309525f484b7e9d6f686b3781b6f61ef925d66d6f6a0", 4900000000]], +"01000000000102fe3dc9208094f3ffd12645477b3dc56f60ec4fa8e6f5d67c565d1c6b9216b36e000000004847304402200af4e47c9b9629dbecc21f73af989bdaa911f7e6f6c2e9394588a3aa68f81e9902204f3fcf6ade7e5abb1295b6774c8e0abd94ae62217367096bc02ee5e435b67da201ffffffff0815cf020f013ed6cf91d29f4202e8a58726b1ac6c79da47c23d1bee0a6925f80000000000ffffffff0100f2052a010000001976a914a30741f8145e5acadf23f751864167f32e0963f788ac000347304402200de66acf4527789bfda55fc5459e214fa6083f936b430a762c629656216805ac0220396f550692cd347171cbc1ef1f51e15282e837bb2b30860dc77c8f78bc8501e503473044022027dc95ad6b740fe5129e7e62a75dd00f291a2aeb1200b84b09d9e3789406b6c002201a9ecd315dd6a0e632ab20bbb98948bc0c6fb204f2c286963bb48517a7058e27034721026dccc749adc2a9d0d89497ac511f760f45c47dc5ed9cf352a58ac706453880aeadab210255a9626aebf5e29c0e6538428ba0d1dcf6ca98ffdf086aa8ced5e0d0215ea465ac00000000", "P2SH,WITNESS"], + +["BIP143 example: P2WSH with unexecuted OP_CODESEPARATOR and SINGLE|ANYONECANPAY"], +[[["01c0cf7fba650638e55eb91261b183251fbb466f90dff17f10086817c542b5e9", 0, "0x00 0x20 0xba468eea561b26301e4cf69fa34bde4ad60c81e70f059f045ca9a79931004a4d", 16777215], +["1b2a9a426ba603ba357ce7773cb5805cb9c7c2b386d100d1fc9263513188e680", 0, "0x00 0x20 0xd9bbfbe56af7c4b7f960a70d7ea107156913d9e5a26b0a71429df5e097ca6537", 16777215]], +"01000000000102e9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff80e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffff0280969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac80969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000", "P2SH,WITNESS"], + +["BIP143 example: Same as the previous example with input-output paris swapped"], +[[["1b2a9a426ba603ba357ce7773cb5805cb9c7c2b386d100d1fc9263513188e680", 0, "0x00 0x20 0xd9bbfbe56af7c4b7f960a70d7ea107156913d9e5a26b0a71429df5e097ca6537", 16777215], +["01c0cf7fba650638e55eb91261b183251fbb466f90dff17f10086817c542b5e9", 0, "0x00 0x20 0xba468eea561b26301e4cf69fa34bde4ad60c81e70f059f045ca9a79931004a4d", 16777215]], +"0100000000010280e68831516392fcd100d186b3c2c7b95c80b53c77e77c35ba03a66b429a2a1b0000000000ffffffffe9b542c5176808107ff1df906f46bb1f2583b16112b95ee5380665ba7fcfc0010000000000ffffffff0280969800000000001976a9146648a8cd4531e1ec47f35916de8e259237294d1e88ac80969800000000001976a914de4b231626ef508c9a74a8517e6783c0546d6b2888ac024730440220032521802a76ad7bf74d0e2c218b72cf0cbc867066e2e53db905ba37f130397e02207709e2188ed7f08f4c952d9d13986da504502b8c3be59617e043552f506c46ff83275163ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac02483045022100f6a10b8604e6dc910194b79ccfc93e1bc0ec7c03453caaa8987f7d6c3413566002206216229ede9b4d6ec2d325be245c5b508ff0339bf1794078e20bfe0babc7ffe683270063ab68210392972e2eb617b2388771abe27235fd5ac44af8e61693261550447a4c3e39da98ac00000000", "P2SH,WITNESS"], + +["BIP143 example: P2SH-P2WSH 6-of-6 multisig signed with 6 different SIGHASH types"], +[[["6eb98797a21c6c10aa74edf29d618be109f48a8e94c694f3701e08ca69186436", 1, "HASH160 0x14 0x9993a429037b5d912407a71c252019287b8d27a5 EQUAL", 987654321]], +"0100000000010136641869ca081e70f394c6948e8af409e18b619df2ed74aa106c1ca29787b96e0100000023220020a16b5755f7f6f96dbd65f5f0d6ab9418b89af4b1f14a1bb8a09062c35f0dcb54ffffffff0200e9a435000000001976a914389ffce9cd9ae88dcc0631e88a821ffdbe9bfe2688acc0832f05000000001976a9147480a33f950689af511e6e84c138dbbd3c3ee41588ac080047304402206ac44d672dac41f9b00e28f4df20c52eeb087207e8d758d76d92c6fab3b73e2b0220367750dbbe19290069cba53d096f44530e4f98acaa594810388cf7409a1870ce01473044022068c7946a43232757cbdf9176f009a928e1cd9a1a8c212f15c1e11ac9f2925d9002205b75f937ff2f9f3c1246e547e54f62e027f64eefa2695578cc6432cdabce271502473044022059ebf56d98010a932cf8ecfec54c48e6139ed6adb0728c09cbe1e4fa0915302e022007cd986c8fa870ff5d2b3a89139c9fe7e499259875357e20fcbb15571c76795403483045022100fbefd94bd0a488d50b79102b5dad4ab6ced30c4069f1eaa69a4b5a763414067e02203156c6a5c9cf88f91265f5a942e96213afae16d83321c8b31bb342142a14d16381483045022100a5263ea0553ba89221984bd7f0b13613db16e7a70c549a86de0cc0444141a407022005c360ef0ae5a5d4f9f2f87a56c1546cc8268cab08c73501d6b3be2e1e1a8a08824730440220525406a1482936d5a21888260dc165497a90a15669636d8edca6b9fe490d309c022032af0c646a34a44d1f4576bf6a4a74b67940f8faa84c7df9abe12a01a11e2b4783cf56210307b8ae49ac90a048e9b53357a2354b3334e9c8bee813ecb98e99a7e07e8c3ba32103b28f0c28bfab54554ae8c658ac5c3e0ce6e79ad336331f78c428dd43eea8449b21034b8113d703413d57761b8b9781957b8c0ac1dfe69f492580ca4195f50376ba4a21033400f6afecb833092a9a21cfdf1ed1376e58c5d1f47de74683123987e967a8f42103a6d48b1131e94ba04d9737d61acdaa1322008af9602b3b14862c07a1789aac162102d8b661b0b3302ee2f162b09e07a55ad5dfbe673a9f01d9f0c19617681024306b56ae00000000", "P2SH,WITNESS"], + ["Make diffs cleaner by leaving a comment here without comma at the end"] ] diff --git a/test/http-test.js b/test/http-test.js index 0c316178..05d8a311 100644 --- a/test/http-test.js +++ b/test/http-test.js @@ -8,6 +8,8 @@ var utils = bcoin.utils; var crypto = require('../lib/crypto/crypto'); var assert = require('assert'); var scriptTypes = constants.scriptTypes; +var co = require('../lib/utils/co'); +var cob = co.cob; var dummyInput = { prevout: { @@ -41,48 +43,39 @@ describe('HTTP', function() { db: 'memory' }); - var wallet = new bcoin.http.wallet({ + var wallet = new bcoin.http.Wallet({ network: 'regtest', apiKey: 'foo' }); node.on('error', function() {}); - it('should open node', function(cb) { + it('should open node', cob(function* () { constants.tx.COINBASE_MATURITY = 0; - node.open(cb); - }); + yield node.open(); + })); - it('should create wallet', function(cb) { - wallet.create({ id: 'test' }, function(err, wallet) { - assert.ifError(err); - assert.equal(wallet.id, 'test'); - cb(); - }); - }); + it('should create wallet', cob(function* () { + var info = yield wallet.create({ id: 'test' }); + assert.equal(info.id, 'test'); + })); - it('should get info', function(cb) { - wallet.client.getInfo(function(err, info) { - assert.ifError(err); - assert.equal(info.network, node.network.type); - assert.equal(info.version, constants.USER_VERSION); - assert.equal(info.agent, constants.USER_AGENT); - assert.equal(info.height, 0); - cb(); - }); - }); + it('should get info', cob(function* () { + var info = yield wallet.client.getInfo(); + assert.equal(info.network, node.network.type); + assert.equal(info.version, constants.USER_VERSION); + assert.equal(info.agent, constants.USER_AGENT); + assert.equal(info.height, 0); + })); - it('should get wallet info', function(cb) { - wallet.getInfo(function(err, wallet) { - assert.ifError(err); - assert.equal(wallet.id, 'test'); - addr = wallet.account.receiveAddress; - assert.equal(typeof addr, 'string'); - cb(); - }); - }); + it('should get wallet info', cob(function* () { + var info = yield wallet.getInfo(); + assert.equal(info.id, 'test'); + addr = info.account.receiveAddress; + assert.equal(typeof addr, 'string'); + })); - it('should fill with funds', function(cb) { + it('should fill with funds', cob(function* () { var balance, receive, details; // Coinbase @@ -107,35 +100,29 @@ describe('HTTP', function() { details = d; }); - node.walletdb.addTX(t1, function(err) { - assert.ifError(err); - setTimeout(function() { - assert(receive); - assert.equal(receive.id, 'test'); - assert.equal(receive.type, 'pubkeyhash'); - assert.equal(receive.change, 0); - assert(balance); - assert.equal(utils.satoshi(balance.confirmed), 0); - assert.equal(utils.satoshi(balance.unconfirmed), 201840); - assert.equal(utils.satoshi(balance.total), 201840); - assert(details); - assert.equal(details.hash, t1.rhash); - cb(); - }, 300); - }); - }); + yield node.walletdb.addTX(t1); + yield co.timeout(300); - it('should get balance', function(cb) { - wallet.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(utils.satoshi(balance.confirmed), 0); - assert.equal(utils.satoshi(balance.unconfirmed), 201840); - assert.equal(utils.satoshi(balance.total), 201840); - cb(); - }); - }); + assert(receive); + assert.equal(receive.id, 'test'); + assert.equal(receive.type, 'pubkeyhash'); + assert.equal(receive.branch, 0); + assert(balance); + assert.equal(utils.satoshi(balance.confirmed), 0); + assert.equal(utils.satoshi(balance.unconfirmed), 201840); + assert.equal(utils.satoshi(balance.total), 201840); + assert(details); + assert.equal(details.hash, t1.rhash); + })); - it('should send a tx', function(cb) { + it('should get balance', cob(function* () { + var balance = yield wallet.getBalance(); + assert.equal(utils.satoshi(balance.confirmed), 0); + assert.equal(utils.satoshi(balance.unconfirmed), 201840); + assert.equal(utils.satoshi(balance.total), 201840); + })); + + it('should send a tx', cob(function* () { var options = { rate: 10000, outputs: [{ @@ -144,47 +131,41 @@ describe('HTTP', function() { }] }; - wallet.send(options, function(err, tx) { - assert.ifError(err); - assert(tx); - assert.equal(tx.inputs.length, 1); - assert.equal(tx.outputs.length, 2); - assert.equal(utils.satoshi(tx.outputs[0].value) + utils.satoshi(tx.outputs[1].value), 48190); - hash = tx.hash; - cb(); - }); - }); + var tx = yield wallet.send(options); - it('should get a tx', function(cb) { - wallet.getTX(hash, function(err, tx) { - assert.ifError(err); - assert(tx); - assert.equal(tx.hash, hash); - cb(); - }); - }); + assert(tx); + assert.equal(tx.inputs.length, 1); + assert.equal(tx.outputs.length, 2); + assert.equal(utils.satoshi(tx.outputs[0].value) + utils.satoshi(tx.outputs[1].value), 48190); + hash = tx.hash; + })); - it('should generate new api key', function(cb) { + it('should get a tx', cob(function* () { + var tx = yield wallet.getTX('default', hash); + assert(tx); + assert.equal(tx.hash, hash); + })); + + it('should generate new api key', cob(function* () { var t = wallet.token.toString('hex'); - wallet.retoken(null, function(err, token) { - assert.ifError(err); - assert(token.length === 64); - assert.notEqual(token, t); - cb(); - }); - }); + var token = yield wallet.retoken(null); + assert(token.length === 64); + assert.notEqual(token, t); + })); - it('should get balance', function(cb) { - wallet.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(utils.satoshi(balance.total), 199570); - cb(); - }); - }); + it('should get balance', cob(function* () { + var balance = yield wallet.getBalance(); + assert.equal(utils.satoshi(balance.total), 199570); + })); - it('should cleanup', function(cb) { + it('should execute an rpc call', cob(function* () { + var info = yield wallet.client.rpc.call('getblockchaininfo', []); + assert.equal(info.blocks, 0); + })); + + it('should cleanup', cob(function* () { constants.tx.COINBASE_MATURITY = 100; - wallet.close(); - node.close(cb); - }); + yield wallet.close(); + yield node.close(); + })); }); diff --git a/test/mempool-test.js b/test/mempool-test.js index 84a88d2a..73fdb002 100644 --- a/test/mempool-test.js +++ b/test/mempool-test.js @@ -7,379 +7,356 @@ var utils = bcoin.utils; var crypto = require('../lib/crypto/crypto'); var assert = require('assert'); var opcodes = constants.opcodes; +var cob = require('../lib/utils/co').cob; + +function dummy(prev, prevHash) { + if (!prevHash) + prevHash = constants.ONE_HASH.toString('hex'); + + return { + prevout: { + hash: prevHash, + index: 0 + }, + coin: { + version: 1, + height: 0, + value: 70000, + script: prev, + coinbase: false, + hash: prevHash, + index: 0 + }, + script: new bcoin.script(), + sequence: 0xffffffff + }; +} describe('Mempool', function() { + var chain, mempool, walletdb; + var wallet, cached; + this.timeout(5000); - var chain = new bcoin.chain({ + chain = new bcoin.chain({ name: 'mp-chain', db: 'memory' }); - var mempool = new bcoin.mempool({ + mempool = new bcoin.mempool({ chain: chain, name: 'mempool-test', db: 'memory' }); - var walletdb = new bcoin.walletdb({ + walletdb = new bcoin.walletdb({ name: 'mempool-wallet-test', db: 'memory', verify: true }); - var w, cached; + it('should open mempool', cob(function* () { + yield mempool.open(); + chain.state.flags |= constants.flags.VERIFY_WITNESS; + })); - mempool.on('error', function() {}); + it('should open walletdb', cob(function* () { + yield walletdb.open(); + })); - it('should open mempool', function(cb) { - mempool.open(function(err) { - assert.ifError(err); - chain.state.flags |= constants.flags.VERIFY_WITNESS; - cb(); - }); - }); + it('should open wallet', cob(function* () { + wallet = yield walletdb.create(); + })); - it('should open walletdb', function(cb) { - walletdb.open(cb); - }); - - it('should open wallet', function(cb) { - walletdb.create({}, function(err, wallet) { - assert.ifError(err); - w = wallet; - cb(); - }); - }); - - it('should handle incoming orphans and TXs', function(cb) { + it('should handle incoming orphans and TXs', cob(function* () { var kp = bcoin.keyring.generate(); - // Coinbase - var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 10000); // 10000 instead of 1000 - var prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); - var dummyInput = { - prevout: { - hash: constants.ONE_HASH.toString('hex'), - index: 0 - }, - coin: { - version: 1, - height: 0, - value: 70000, - script: prev, - coinbase: false, - hash: constants.ONE_HASH.toString('hex'), - index: 0 - }, - script: new bcoin.script([]), - sequence: 0xffffffff - }; - t1.addInput(dummyInput); - t1.inputs[0].script = new bcoin.script([t1.signature(0, prev, kp.privateKey, 'all', 0)]), + var w = wallet; + var t1, t2, t3, t4, f1, fake, prev, sig, balance, txs; + + t1 = bcoin.mtx() + .addOutput(w.getAddress(), 50000) + .addOutput(w.getAddress(), 10000); + + prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); + t1.addInput(dummy(prev)); + sig = t1.signature(0, prev, kp.privateKey, 'all', 0); + t1.inputs[0].script = new bcoin.script([sig]), // balance: 51000 - w.sign(t1, function(err, total) { - assert.ifError(err); - t1 = t1.toTX(); - var t2 = bcoin.mtx().addInput(t1, 0) // 50000 - .addOutput(w, 20000) - .addOutput(w, 20000); - // balance: 49000 - w.sign(t2, function(err, total) { - assert.ifError(err); - t2 = t2.toTX(); - var t3 = bcoin.mtx().addInput(t1, 1) // 10000 - .addInput(t2, 0) // 20000 - .addOutput(w, 23000); - // balance: 47000 - w.sign(t3, function(err, total) { - assert.ifError(err); - t3 = t3.toTX(); - var t4 = bcoin.mtx().addInput(t2, 1) // 24000 - .addInput(t3, 0) // 23000 - .addOutput(w, 11000) - .addOutput(w, 11000); - // balance: 22000 - w.sign(t4, function(err, total) { - assert.ifError(err); - t4 = t4.toTX(); - var f1 = bcoin.mtx().addInput(t4, 1) // 11000 - .addOutput(bcoin.address.fromData(new Buffer([])).toBase58(), 9000); - // balance: 11000 - w.sign(f1, function(err, total) { - assert.ifError(err); - f1 = f1.toTX(); - var fake = bcoin.mtx().addInput(t1, 1) // 1000 (already redeemed) - .addOutput(w, 6000); // 6000 instead of 500 - // Script inputs but do not sign - w.template(fake, function(err) { - assert.ifError(err); - // Fake signature - fake.inputs[0].script.set(0, new Buffer([0,0,0,0,0,0,0,0,0])); - fake.inputs[0].script.compile(); - fake = fake.toTX(); - // balance: 11000 - [t2, t3, t4, f1, fake].forEach(function(tx) { - tx.inputs.forEach(function(input) { - delete input.coin; - }); - }); + yield w.sign(t1); + t1 = t1.toTX(); - mempool.addTX(fake, function(err) { - assert.ifError(err); - mempool.addTX(t4, function(err) { - assert.ifError(err); - var balance = mempool.getBalance(); - assert.equal(balance, 0); - mempool.addTX(t1, function(err) { - assert.ifError(err); - var balance = mempool.getBalance(); - assert.equal(balance, 60000); - mempool.addTX(t2, function(err) { - assert.ifError(err); - var balance = mempool.getBalance(); - assert.equal(balance, 50000); - mempool.addTX(t3, function(err) { - assert.ifError(err); - var balance = mempool.getBalance(); - assert.equal(balance, 22000); - mempool.addTX(f1, function(err) { - assert.ifError(err); - var balance = mempool.getBalance(); - assert.equal(balance, 20000); - var txs = mempool.getHistory(); - assert(txs.some(function(tx) { - return tx.hash('hex') === f1.hash('hex'); - })); + t2 = bcoin.mtx() + .addInput(t1, 0) // 50000 + .addOutput(w.getAddress(), 20000) + .addOutput(w.getAddress(), 20000); - cb(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); + // balance: 49000 + yield w.sign(t2); + t2 = t2.toTX(); + + t3 = bcoin.mtx() + .addInput(t1, 1) // 10000 + .addInput(t2, 0) // 20000 + .addOutput(w.getAddress(), 23000); + + // balance: 47000 + yield w.sign(t3); + t3 = t3.toTX(); + + t4 = bcoin.mtx() + .addInput(t2, 1) // 24000 + .addInput(t3, 0) // 23000 + .addOutput(w.getAddress(), 11000) + .addOutput(w.getAddress(), 11000); + + // balance: 22000 + yield w.sign(t4); + t4 = t4.toTX(); + + f1 = bcoin.mtx() + .addInput(t4, 1) // 11000 + .addOutput(new bcoin.address(), 9000); + + // balance: 11000 + yield w.sign(f1); + f1 = f1.toTX(); + + fake = bcoin.mtx() + .addInput(t1, 1) // 1000 (already redeemed) + .addOutput(w.getAddress(), 6000); // 6000 instead of 500 + + // Script inputs but do not sign + yield w.template(fake); + + // Fake signature + fake.inputs[0].script.set(0, constants.ZERO_SIG); + fake.inputs[0].script.compile(); + fake = fake.toTX(); + // balance: 11000 + + [t2, t3, t4, f1, fake].forEach(function(tx) { + tx.inputs.forEach(function(input) { + input.coin = null; }); }); - }); - it('should handle locktime', function(cb) { + yield mempool.addTX(fake); + yield mempool.addTX(t4); + + balance = mempool.getBalance(); + assert.equal(balance, 0); + + yield mempool.addTX(t1); + + balance = mempool.getBalance(); + assert.equal(balance, 60000); + + yield mempool.addTX(t2); + + balance = mempool.getBalance(); + assert.equal(balance, 50000); + + yield mempool.addTX(t3); + + balance = mempool.getBalance(); + assert.equal(balance, 22000); + + yield mempool.addTX(f1); + + balance = mempool.getBalance(); + assert.equal(balance, 20000); + + txs = mempool.getHistory(); + assert(txs.some(function(tx) { + return tx.hash('hex') === f1.hash('hex'); + })); + })); + + it('should handle locktime', cob(function* () { + var w = wallet; var kp = bcoin.keyring.generate(); - // Coinbase - var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 10000); // 10000 instead of 1000 - var prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); - var prevHash = crypto.randomBytes(32).toString('hex'); - var dummyInput = { - prevout: { - hash: prevHash, - index: 0 - }, - coin: { - version: 1, - height: 0, - value: 70000, - script: prev, - coinbase: false, - hash: prevHash, - index: 0 - }, - script: new bcoin.script([]), - sequence: 0xffffffff - }; - t1.addInput(dummyInput); - t1.setLocktime(200); + var tx, prev, prevHash, sig; + + tx = bcoin.mtx() + .addOutput(w.getAddress(), 50000) + .addOutput(w.getAddress(), 10000); + + prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); + prevHash = crypto.randomBytes(32).toString('hex'); + + tx.addInput(dummy(prev, prevHash)); + tx.setLocktime(200); + chain.tip.height = 200; - t1.inputs[0].script = new bcoin.script([t1.signature(0, prev, kp.privateKey, 'all', 0)]), - t1 = t1.toTX(); - mempool.addTX(t1, function(err) { - chain.tip.height = 0; - assert.ifError(err); - cb(); - }); - }); - it('should handle invalid locktime', function(cb) { + sig = tx.signature(0, prev, kp.privateKey, 'all', 0); + tx.inputs[0].script = new bcoin.script([sig]), + + tx = tx.toTX(); + + yield mempool.addTX(tx); + chain.tip.height = 0; + })); + + it('should handle invalid locktime', cob(function* () { + var w = wallet; var kp = bcoin.keyring.generate(); - // Coinbase - var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 10000); // 10000 instead of 1000 - var prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); - var prevHash = crypto.randomBytes(32).toString('hex'); - var dummyInput = { - prevout: { - hash: prevHash, - index: 0 - }, - coin: { - version: 1, - height: 0, - value: 70000, - script: prev, - coinbase: false, - hash: prevHash, - index: 0 - }, - script: new bcoin.script([]), - sequence: 0xffffffff - }; - t1.addInput(dummyInput); - t1.setLocktime(200); + var tx, prev, prevHash, sig, err; + + tx = bcoin.mtx() + .addOutput(w.getAddress(), 50000) + .addOutput(w.getAddress(), 10000); + + prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); + prevHash = crypto.randomBytes(32).toString('hex'); + + tx.addInput(dummy(prev, prevHash)); + tx.setLocktime(200); chain.tip.height = 200 - 1; - t1.inputs[0].script = new bcoin.script([t1.signature(0, prev, kp.privateKey, 'all', 0)]), - t1 = t1.toTX(); - mempool.addTX(t1, function(err) { - chain.tip.height = 0; - assert(err); - cb(); - }); - }); - it('should not cache a malleated wtx with mutated sig', function(cb) { + sig = tx.signature(0, prev, kp.privateKey, 'all', 0); + tx.inputs[0].script = new bcoin.script([sig]), + tx = tx.toTX(); + + try { + yield mempool.addTX(tx); + } catch (e) { + err = e; + } + + assert(err); + + chain.tip.height = 0; + })); + + it('should not cache a malleated wtx with mutated sig', cob(function* () { + var w = wallet; var kp = bcoin.keyring.generate(); + var tx, prev, prevHash, prevs, sig, tx, err; + kp.witness = true; - // Coinbase - var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 10000); // 10000 instead of 1000 - var prev = new bcoin.script([0, kp.keyHash]); - var prevHash = crypto.randomBytes(32).toString('hex'); - var dummyInput = { - prevout: { - hash: prevHash, - index: 0 - }, - coin: { - version: 1, - height: 0, - value: 70000, - script: prev, - coinbase: false, - hash: prevHash, - index: 0 - }, - script: new bcoin.script([]), - sequence: 0xffffffff - }; - t1.addInput(dummyInput); - var prevs = bcoin.script.fromPubkeyhash(kp.keyHash); - var sig = new bcoin.witness([t1.signature(0, prevs, kp.privateKey, 'all', 1), kp.publicKey]); - var sig2 = new bcoin.witness([t1.signature(0, prevs, kp.privateKey, 'all', 1), kp.publicKey]); - sig2.items[0][sig2.items[0].length - 1] = 0; - t1.inputs[0].witness = sig2; - var tx = t1.toTX(); - mempool.addTX(tx, function(err) { - assert(err); - assert(!mempool.hasReject(tx.hash())); - cb(); - }); - }); - it('should not cache a malleated tx with unnecessary witness', function(cb) { - var kp = bcoin.keyring.generate(); - // Coinbase - var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 10000); // 10000 instead of 1000 - var prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); - var prevHash = crypto.randomBytes(32).toString('hex'); - var dummyInput = { - prevout: { - hash: prevHash, - index: 0 - }, - coin: { - version: 1, - height: 0, - value: 70000, - script: prev, - coinbase: false, - hash: prevHash, - index: 0 - }, - script: new bcoin.script([]), - sequence: 0xffffffff - }; - t1.addInput(dummyInput); - t1.inputs[0].script = new bcoin.script([t1.signature(0, prev, kp.privateKey, 'all', 0)]), - t1.inputs[0].witness.push(new Buffer(0)); - var tx = t1.toTX(); - mempool.addTX(tx, function(err) { - assert(err); - assert(!mempool.hasReject(tx.hash())); - cb(); - }); - }); + tx = bcoin.mtx() + .addOutput(w.getAddress(), 50000) + .addOutput(w.getAddress(), 10000); - it('should not cache a malleated wtx with wit removed', function(cb) { + prev = new bcoin.script([0, kp.keyHash]); + prevHash = crypto.randomBytes(32).toString('hex'); + + tx.addInput(dummy(prev, prevHash)); + + prevs = bcoin.script.fromPubkeyhash(kp.keyHash); + + sig = tx.signature(0, prevs, kp.privateKey, 'all', 1); + sig[sig.length - 1] = 0; + tx.inputs[0].witness = new bcoin.witness([sig, kp.publicKey]); + tx = tx.toTX(); + + try { + yield mempool.addTX(tx); + } catch (e) { + err = e; + } + + assert(err); + assert(!mempool.hasReject(tx.hash())); + })); + + it('should not cache a malleated tx with unnecessary witness', cob(function* () { + var w = wallet; var kp = bcoin.keyring.generate(); + var tx, prev, prevHash, sig, tx, err; + + tx = bcoin.mtx() + .addOutput(w.getAddress(), 50000) + .addOutput(w.getAddress(), 10000); + + prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); + prevHash = crypto.randomBytes(32).toString('hex'); + + tx.addInput(dummy(prev, prevHash)); + + sig = tx.signature(0, prev, kp.privateKey, 'all', 0); + tx.inputs[0].script = new bcoin.script([sig]); + tx.inputs[0].witness.push(new Buffer(0)); + tx = tx.toTX(); + + try { + yield mempool.addTX(tx); + } catch (e) { + err = e; + } + + assert(err); + assert(!mempool.hasReject(tx.hash())); + })); + + it('should not cache a malleated wtx with wit removed', cob(function* () { + var w = wallet; + var kp = bcoin.keyring.generate(); + var tx, prev, prevHash, tx, err; + kp.witness = true; - // Coinbase - var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 10000); // 10000 instead of 1000 - var prev = new bcoin.script([0, kp.keyHash]); - var prevHash = crypto.randomBytes(32).toString('hex'); - var dummyInput = { - prevout: { - hash: prevHash, - index: 0 - }, - coin: { - version: 1, - height: 0, - value: 70000, - script: prev, - coinbase: false, - hash: prevHash, - index: 0 - }, - script: new bcoin.script([]), - sequence: 0xffffffff - }; - t1.addInput(dummyInput); - var tx = t1.toTX(); - mempool.addTX(tx, function(err) { - assert(err); - assert(err.malleated); - assert(!mempool.hasReject(tx.hash())); - cb(); - }); - }); - it('should cache non-malleated tx without sig', function(cb) { + tx = bcoin.mtx() + .addOutput(w.getAddress(), 50000) + .addOutput(w.getAddress(), 10000); + + prev = new bcoin.script([0, kp.keyHash]); + prevHash = crypto.randomBytes(32).toString('hex'); + + tx.addInput(dummy(prev, prevHash)); + + tx = tx.toTX(); + + try { + yield mempool.addTX(tx); + } catch (e) { + err = e; + } + + assert(err); + assert(err.malleated); + assert(!mempool.hasReject(tx.hash())); + })); + + it('should cache non-malleated tx without sig', cob(function* () { + var w = wallet; var kp = bcoin.keyring.generate(); - // Coinbase - var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 10000); // 10000 instead of 1000 - var prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); - var prevHash = crypto.randomBytes(32).toString('hex'); - var dummyInput = { - prevout: { - hash: prevHash, - index: 0 - }, - coin: { - version: 1, - height: 0, - value: 70000, - script: prev, - coinbase: false, - hash: prevHash, - index: 0 - }, - script: new bcoin.script([]), - sequence: 0xffffffff - }; - t1.addInput(dummyInput); - var tx = t1.toTX(); - mempool.addTX(tx, function(err) { - assert(err); - assert(!err.malleated); - assert(mempool.hasReject(tx.hash())); - cached = tx; - cb(); - }); - }); + var tx, prev, prevHash, tx, err; - it('should clear reject cache', function(cb) { - var t1 = bcoin.mtx().addOutput(w, 50000); - var dummyInput = { + tx = bcoin.mtx() + .addOutput(w.getAddress(), 50000) + .addOutput(w.getAddress(), 10000); + + prev = new bcoin.script([kp.publicKey, opcodes.OP_CHECKSIG]); + prevHash = crypto.randomBytes(32).toString('hex'); + + tx.addInput(dummy(prev, prevHash)); + + tx = tx.toTX(); + + try { + yield mempool.addTX(tx); + } catch (e) { + err = e; + } + + assert(err); + assert(!err.malleated); + assert(mempool.hasReject(tx.hash())); + cached = tx; + })); + + it('should clear reject cache', cob(function* () { + var w = wallet; + var tx, input, tx, block; + + tx = bcoin.mtx() + .addOutput(w.getAddress(), 50000); + + input = { prevout: { hash: constants.NULL_HASH, index: 0xffffffff @@ -388,19 +365,20 @@ describe('Mempool', function() { script: new bcoin.script(), sequence: 0xffffffff }; - t1.addInput(dummyInput); - var tx = t1.toTX(); - var block = new bcoin.block(); - block.txs.push(tx); - assert(mempool.hasReject(cached.hash())); - mempool.addBlock(block, function(err) { - assert(!err); - assert(!mempool.hasReject(cached.hash())); - cb(); - }); - }); - it('should destroy mempool', function(cb) { - mempool.close(cb); - }); + tx.addInput(input); + + tx = tx.toTX(); + + block = new bcoin.block(); + block.txs.push(tx); + + assert(mempool.hasReject(cached.hash())); + yield mempool.addBlock(block); + assert(!mempool.hasReject(cached.hash())); + })); + + it('should destroy mempool', cob(function* () { + yield mempool.close(); + })); }); diff --git a/test/script-test.js b/test/script-test.js index e7955faa..06b1ae22 100644 --- a/test/script-test.js +++ b/test/script-test.js @@ -67,7 +67,7 @@ describe('Script', function() { inputScript.execute(stack); var res = prevOutScript.execute(stack); assert(res); - assert.deepEqual(stack.slice(), [[1], [3], [5]]); + assert.deepEqual(stack.items, [[1], [3], [5]]); var inputScript = new Script([opcodes.OP_1, opcodes.OP_2]); var prevOutScript = new Script([ @@ -84,7 +84,7 @@ describe('Script', function() { inputScript.execute(stack); var res = prevOutScript.execute(stack); assert(res); - assert.deepEqual(stack.slice(), [[1], [4], [5]]); + assert.deepEqual(stack.items, [[1], [4], [5]]); var inputScript = new Script([opcodes.OP_1, opcodes.OP_2]); var prevOutScript = new Script([ @@ -99,7 +99,7 @@ describe('Script', function() { inputScript.execute(stack); var res = prevOutScript.execute(stack); assert(res); - assert.deepEqual(stack.slice(), [[1], [3], [5]]); + assert.deepEqual(stack.items, [[1], [3], [5]]); var inputScript = new Script([opcodes.OP_1, opcodes.OP_2]); var prevOutScript = new Script([ @@ -114,7 +114,7 @@ describe('Script', function() { inputScript.execute(stack); var res = prevOutScript.execute(stack); assert(res); - assert.deepEqual(stack.slice(), [[1], [5]]); + assert.deepEqual(stack.items, [[1], [5]]); var inputScript = new Script([opcodes.OP_1, opcodes.OP_2]); var prevOutScript = new Script([ @@ -129,7 +129,7 @@ describe('Script', function() { inputScript.execute(stack); var res = prevOutScript.execute(stack); assert(res); - assert.deepEqual(stack.slice(), [[1], [3], [5]]); + assert.deepEqual(stack.items, [[1], [3], [5]]); }); function success(res, stack) { @@ -380,7 +380,7 @@ describe('Script', function() { assert.equal(err.code, expected); return; } - utils.assert.ifError(err); + assert.ifError(err); assert(res); }); }); diff --git a/test/wallet-test.js b/test/wallet-test.js index 7f9e7123..4143f0fe 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -8,19 +8,15 @@ var utils = bcoin.utils; var crypto = require('../lib/crypto/crypto'); var assert = require('assert'); var scriptTypes = constants.scriptTypes; - -var FAKE_SIG = new Buffer([0,0,0,0,0,0,0,0,0]); +var co = require('../lib/utils/co'); +var cob = co.cob; var KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ'; -KEY1 = { xprivkey: KEY1 }; - var KEY2 = 'xprv9s21ZrQH143K3mqiSThzPtWAabQ22Pjp3uSNnZ53A5bQ4udp' + 'faKekc2m4AChLYH1XDzANhrSdxHYWUeTWjYJwFwWFyHkTMnMeAcW4JyRCZa'; -KEY2 = { xprivkey: KEY2 }; - var dummyInput = { prevout: { hash: constants.NULL_HASH, @@ -40,39 +36,28 @@ var dummyInput = { sequence: 0xffffffff }; -assert.range = function range(value, lo, hi, message) { - if (!(value >= lo && value <= hi)) { - throw new assert.AssertionError({ - message: message, - actual: value, - expected: lo + ', ' + hi, - operator: '>= && <=', - stackStartFunction: range - }); - } -}; - describe('Wallet', function() { - var walletdb = new bcoin.walletdb({ + var walletdb, wallet, ewallet, ekey, doubleSpendWallet, doubleSpend; + + walletdb = new bcoin.walletdb({ name: 'wallet-test', db: 'memory', verify: true }); - var lastW; - it('should open walletdb', function(cb) { + this.timeout(5000); + + it('should open walletdb', cob(function* () { constants.tx.COINBASE_MATURITY = 0; - walletdb.open(cb); - }); + yield walletdb.open(); + })); - it('should generate new key and address', function() { - walletdb.create(function(err, w) { - assert.ifError(err); - var addr = w.getAddress('base58'); - assert(addr); - assert(bcoin.address.validate(addr)); - }); - }); + it('should generate new key and address', cob(function* () { + var w = yield walletdb.create(); + var addr = w.getAddress('base58'); + assert(addr); + assert(bcoin.address.validate(addr)); + })); it('should validate existing address', function() { assert(bcoin.address.validate('1KQ1wMNwXHUYj1nV2xzsRcKUH8gVFpTFUc')); @@ -82,1046 +67,965 @@ describe('Wallet', function() { assert(!bcoin.address.validate('1KQ1wMNwXHUYj1nv2xzsRcKUH8gVFpTFUc')); }); - it('should create and get wallet', function(cb) { - walletdb.create(function(err, w1) { - assert.ifError(err); - w1.destroy(); - walletdb.get(w1.id, function(err, w1_) { - assert.ifError(err); - // assert(w1 !== w1_); - // assert(w1.master !== w1_.master); - assert.equal(w1.master.key.xprivkey, w1.master.key.xprivkey); - // assert(w1.account !== w1_.account); - assert.equal(w1.account.accountKey.xpubkey, w1.account.accountKey.xpubkey); - cb(); - }); - }); - }); + it('should create and get wallet', cob(function* () { + var w1, w2; - function p2pkh(witness, bullshitNesting, cb) { + w1 = yield walletdb.create(); + yield w1.destroy(); + + w2 = yield walletdb.get(w1.id); + + assert(w1 !== w2); + assert(w1.master !== w2.master); + assert.equal(w1.master.key.xprivkey, w2.master.key.xprivkey); + assert.equal(w1.account.accountKey.xpubkey, w2.account.accountKey.xpubkey); + })); + + var p2pkh = co(function* p2pkh(witness, bullshitNesting) { var flags = bcoin.constants.flags.STANDARD_VERIFY_FLAGS; + var w, addr, src, tx; if (witness) flags |= bcoin.constants.flags.VERIFY_WITNESS; - walletdb.create({ witness: witness }, function(err, w) { - assert.ifError(err); + w = yield walletdb.create({ witness: witness }); - var ad = bcoin.address.fromBase58(w.getAddress('base58')); + addr = bcoin.address.fromBase58(w.getAddress('base58')); - if (witness) - assert(ad.type === scriptTypes.WITNESSPUBKEYHASH); - else - assert(ad.type === scriptTypes.PUBKEYHASH); + if (witness) + assert.equal(addr.type, scriptTypes.WITNESSPUBKEYHASH); + else + assert.equal(addr.type, scriptTypes.PUBKEYHASH); - var src = bcoin.mtx({ - outputs: [{ - value: 5460 * 2, - address: bullshitNesting - ? w.getProgramAddress() - : w.getAddress() - }, { - value: 5460 * 2, - address: bcoin.address.fromData(new Buffer([])).toBase58() - }] - }); - - src.addInput(dummyInput); - - var tx = bcoin.mtx() - .addInput(src, 0) - .addOutput(w.getAddress(), 5460); - - w.sign(tx, function(err) { - assert.ifError(err); - assert(tx.verify(flags)); - cb(); - }); + src = bcoin.mtx({ + outputs: [{ + value: 5460 * 2, + address: bullshitNesting + ? w.getNestedAddress() + : w.getAddress() + }, { + value: 5460 * 2, + address: new bcoin.address() + }] }); - } - it('should sign/verify pubkeyhash tx', function(cb) { - p2pkh(false, false, cb); + src.addInput(dummyInput); + + tx = bcoin.mtx() + .addInput(src, 0) + .addOutput(w.getAddress(), 5460); + + yield w.sign(tx); + + assert(tx.verify(flags)); }); - it('should sign/verify witnesspubkeyhash tx', function(cb) { - p2pkh(true, false, cb); - }); + it('should sign/verify pubkeyhash tx', cob(function* () { + yield p2pkh(false, false); + })); - it('should sign/verify witnesspubkeyhash tx with bullshit nesting', function(cb) { - p2pkh(true, true, cb); - }); + it('should sign/verify witnesspubkeyhash tx', cob(function* () { + yield p2pkh(true, false); + })); - it('should multisign/verify TX', function(cb) { - walletdb.create({ + it('should sign/verify witnesspubkeyhash tx with bullshit nesting', cob(function* () { + yield p2pkh(true, true); + })); + + it('should multisign/verify TX', cob(function* () { + var w, k, keys, src, tx, maxSize; + + w = yield walletdb.create({ type: 'multisig', m: 1, n: 2 - }, function(err, w) { - assert.ifError(err); - var k2 = bcoin.hd.fromMnemonic().deriveAccount44(0).hdPublicKey; - w.addKey(k2, function(err) { - assert.ifError(err); - var keys = [ - w.getPublicKey(), - k2.derive('m/0/0').publicKey - ]; - // Input transaction (bare 1-of-2 multisig) - var src = bcoin.mtx({ - outputs: [{ - value: 5460 * 2, - script: bcoin.script.fromMultisig(1, 2, keys) - }, { - value: 5460 * 2, - address: bcoin.address.fromData(new Buffer([])).toBase58() - }] - }); - src.addInput(dummyInput); - - var tx = bcoin.mtx() - .addInput(src, 0) - .addOutput(w.getAddress(), 5460); - - var maxSize = tx.maxSize(); - w.sign(tx, function(err) { - assert.ifError(err); - assert(tx.toRaw().length <= maxSize); - assert(tx.verify()); - cb(); - }); - }); }); - }); - var dw, di; - it('should have TX pool and be serializable', function(cb) { - walletdb.create(function(err, w) { - assert.ifError(err); - walletdb.create(function(err, f) { - assert.ifError(err); - dw = w; + k = bcoin.hd.fromMnemonic().deriveAccount44(0).hdPublicKey; - // Coinbase - var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 1000); - t1.addInput(dummyInput); - // balance: 51000 - w.sign(t1, function(err) { - assert.ifError(err); - t1 = t1.toTX(); - var t2 = bcoin.mtx().addInput(t1, 0) // 50000 - .addOutput(w, 24000) - .addOutput(w, 24000); - di = t2.inputs[0]; - // balance: 49000 - w.sign(t2, function(err) { - assert.ifError(err); - t2 = t2.toTX(); - var t3 = bcoin.mtx().addInput(t1, 1) // 1000 - .addInput(t2, 0) // 24000 - .addOutput(w, 23000); - // balance: 47000 - w.sign(t3, function(err) { - assert.ifError(err); - t3 = t3.toTX(); - var t4 = bcoin.mtx().addInput(t2, 1) // 24000 - .addInput(t3, 0) // 23000 - .addOutput(w, 11000) - .addOutput(w, 11000); - // balance: 22000 - w.sign(t4, function(err) { - assert.ifError(err); - t4 = t4.toTX(); - var f1 = bcoin.mtx().addInput(t4, 1) // 11000 - .addOutput(f, 10000); - // balance: 11000 - w.sign(f1, function(err) { - assert.ifError(err); - f1 = f1.toTX(); - var fake = bcoin.mtx().addInput(t1, 1) // 1000 (already redeemed) - .addOutput(w, 500); - // Script inputs but do not sign - w.template(fake, function(err) { - assert.ifError(err); - // Fake signature - fake.inputs[0].script.set(0, FAKE_SIG); - fake.inputs[0].script.compile(); - // balance: 11000 - fake = fake.toTX(); + yield w.addKey(k); - // Fake TX should temporarly change output - walletdb.addTX(fake, function(err) { - assert.ifError(err); - walletdb.addTX(t4, function(err) { - assert.ifError(err); - w.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(balance.total, 22500); - walletdb.addTX(t1, function(err) { - w.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(balance.total, 73000); - walletdb.addTX(t2, function(err) { - assert.ifError(err); - w.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(balance.total, 47000); - walletdb.addTX(t3, function(err) { - assert.ifError(err); - w.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(balance.total, 22000); - walletdb.addTX(f1, function(err) { - assert.ifError(err); - w.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(balance.total, 11000); - w.getHistory(function(err, txs) { - assert(txs.some(function(tx) { - return tx.hash('hex') === f1.hash('hex'); - })); - f.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(balance.total, 10000); - f.getHistory(function(err, txs) { - assert.ifError(err); - assert(txs.some(function(tx) { - return tx.hash('hex') === f1.hash('hex'); - })); - cb(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); + keys = [ + w.getPublicKey(), + k.derive('m/0/0').publicKey + ]; + + // Input transaction (bare 1-of-2 multisig) + src = bcoin.mtx({ + outputs: [{ + value: 5460 * 2, + script: bcoin.script.fromMultisig(1, 2, keys) + }, { + value: 5460 * 2, + address: new bcoin.address() + }] }); - }); - it('should cleanup spenders after double-spend', function(cb) { - var t1 = bcoin.mtx().addOutput(dw, 5000); - t1.addInput(di.coin); - dw.getHistory(function(err, txs) { - assert.ifError(err); - assert.equal(txs.length, 5); - var total = txs.reduce(function(t, tx) { - return t + tx.getOutputValue(); - }, 0); - assert.equal(total, 154000); - dw.getCoins(function(err, coins) { - assert.ifError(err); - dw.sign(t1, function(err) { - assert.ifError(err); - t1 = t1.toTX(); - dw.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(balance.total, 11000); - walletdb.addTX(t1, function(err) { - assert.ifError(err); - dw.getCoins(function(err, coins) { - assert.ifError(err); - dw.getBalance(function(err, balance) { - assert.ifError(err); - assert.equal(balance.total, 6000); - dw.getHistory(function(err, txs) { - assert.ifError(err); - assert.equal(txs.length, 2); - var total = txs.reduce(function(t, tx) { - return t + tx.getOutputValue(); - }, 0); - assert.equal(total, 56000); - cb(); - }); - }); - }); - }); - }); - }); - }); + src.addInput(dummyInput); + + tx = bcoin.mtx() + .addInput(src, 0) + .addOutput(w.getAddress(), 5460); + + maxSize = tx.maxSize(); + + yield w.sign(tx); + + assert(tx.toRaw().length <= maxSize); + assert(tx.verify()); + })); + + it('should have TX pool and be serializable', cob(function* () { + var w = yield walletdb.create(); + var f = yield walletdb.create(); + var t1, t2, t3, t4, f1, fake, balance, txs; + + doubleSpendWallet = w; + + // Coinbase + t1 = bcoin.mtx() + .addOutput(w.getAddress(), 50000) + .addOutput(w.getAddress(), 1000); + t1.addInput(dummyInput); + + // balance: 51000 + yield w.sign(t1); + t1 = t1.toTX(); + + t2 = bcoin.mtx() + .addInput(t1, 0) // 50000 + .addOutput(w.getAddress(), 24000) + .addOutput(w.getAddress(), 24000); + + doubleSpend = t2.inputs[0]; + + // balance: 49000 + yield w.sign(t2); + t2 = t2.toTX(); + t3 = bcoin.mtx() + .addInput(t1, 1) // 1000 + .addInput(t2, 0) // 24000 + .addOutput(w.getAddress(), 23000); + + // balance: 47000 + yield w.sign(t3); + t3 = t3.toTX(); + t4 = bcoin.mtx() + .addInput(t2, 1) // 24000 + .addInput(t3, 0) // 23000 + .addOutput(w.getAddress(), 11000) + .addOutput(w.getAddress(), 11000); + + // balance: 22000 + yield w.sign(t4); + t4 = t4.toTX(); + f1 = bcoin.mtx() + .addInput(t4, 1) // 11000 + .addOutput(f.getAddress(), 10000); + + // balance: 11000 + yield w.sign(f1); + f1 = f1.toTX(); + fake = bcoin.mtx() + .addInput(t1, 1) // 1000 (already redeemed) + .addOutput(w.getAddress(), 500); + + // Script inputs but do not sign + yield w.template(fake); + // Fake signature + fake.inputs[0].script.set(0, constants.ZERO_SIG); + fake.inputs[0].script.compile(); + // balance: 11000 + fake = fake.toTX(); + + // Fake TX should temporarly change output + yield walletdb.addTX(fake); + + yield walletdb.addTX(t4); + + balance = yield w.getBalance(); + assert.equal(balance.total, 22500); + + yield walletdb.addTX(t1); + + balance = yield w.getBalance(); + assert.equal(balance.total, 73000); + + yield walletdb.addTX(t2); + + balance = yield w.getBalance(); + assert.equal(balance.total, 47000); + + yield walletdb.addTX(t3); + + balance = yield w.getBalance(); + assert.equal(balance.total, 22000); + + yield walletdb.addTX(f1); + + balance = yield w.getBalance(); + assert.equal(balance.total, 11000); + + txs = yield w.getHistory(); + assert(txs.some(function(tx) { + return tx.hash('hex') === f1.hash('hex'); + })); + + balance = yield f.getBalance(); + assert.equal(balance.total, 10000); + + txs = yield f.getHistory(); + assert(txs.some(function(tx) { + return tx.hash('hex') === f1.hash('hex'); + })); + })); + + it('should cleanup spenders after double-spend', cob(function* () { + var w = doubleSpendWallet; + var tx, txs, total, balance; + + tx = bcoin.mtx().addOutput(w.getAddress(), 5000); + tx.addInput(doubleSpend.coin); + + txs = yield w.getHistory(); + assert.equal(txs.length, 5); + total = txs.reduce(function(t, tx) { + return t + tx.getOutputValue(); + }, 0); + + assert.equal(total, 154000); + + yield w.sign(tx); + tx = tx.toTX(); + + balance = yield w.getBalance(); + assert.equal(balance.total, 11000); + + yield walletdb.addTX(tx); + + balance = yield w.getBalance(); + assert.equal(balance.total, 6000); + + txs = yield w.getHistory(); + assert.equal(txs.length, 2); + + total = txs.reduce(function(t, tx) { + return t + tx.getOutputValue(); + }, 0); + assert.equal(total, 56000); + })); + + it('should fill tx with inputs', cob(function* () { + var w1 = yield walletdb.create(); + var w2 = yield walletdb.create(); + var t1, t2, t3, err; + + // Coinbase + t1 = bcoin.mtx() + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460); + + t1.addInput(dummyInput); + t1 = t1.toTX(); + + yield walletdb.addTX(t1); + + // Create new transaction + t2 = bcoin.mtx().addOutput(w2.getAddress(), 5460); + yield w1.fund(t2, { rate: 10000, round: true }); + yield w1.sign(t2); + t2 = t2.toTX(); + + assert(t2.verify()); + + assert.equal(t2.getInputValue(), 16380); + assert.equal(t2.getOutputValue(), 5460); + assert.equal(t2.getFee(), 10920); + + // Create new transaction + t3 = bcoin.mtx().addOutput(w2.getAddress(), 15000); + + try { + yield w1.fund(t3, { rate: 10000, round: true }); + } catch (e) { + err = e; + } + + assert(err); + assert.equal(err.requiredFunds, 25000); + })); + + it('should fill tx with inputs with accurate fee', cob(function* () { + var w1 = yield walletdb.create({ master: KEY1 }); + var w2 = yield walletdb.create({ master: KEY2 }); + var t1, t2, t3, balance, err; + + // Coinbase + t1 = bcoin.mtx() + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460); + + t1.addInput(dummyInput); + t1 = t1.toTX(); + + yield walletdb.addTX(t1); + + // Create new transaction + t2 = bcoin.mtx().addOutput(w2.getAddress(), 5460); + yield w1.fund(t2, { rate: 10000 }); + + yield w1.sign(t2); + t2 = t2.toTX(); + assert(t2.verify()); + + assert.equal(t2.getInputValue(), 16380); + + // Should now have a change output: + assert.equal(t2.getOutputValue(), 11130); + + assert.equal(t2.getFee(), 5250); + + assert.equal(t2.getWeight(), 2084); + assert.equal(t2.getBaseSize(), 521); + assert.equal(t2.getSize(), 521); + assert.equal(t2.getVirtualSize(), 521); + + w2.once('balance', function(b) { + balance = b; }); - }); - it('should fill tx with inputs', function(cb) { - walletdb.create(function(err, w1) { - assert.ifError(err); - walletdb.create(function(err, w2) { - assert.ifError(err); + yield walletdb.addTX(t2); - // Coinbase - var t1 = bcoin.mtx() - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460); + // Create new transaction + t3 = bcoin.mtx().addOutput(w2.getAddress(), 15000); - t1.addInput(dummyInput); - t1 = t1.toTX(); + try { + yield w1.fund(t3, { rate: 10000 }); + } catch (e) { + err = e; + } - walletdb.addTX(t1, function(err) { - assert.ifError(err); + assert(err); + assert(balance); + assert(balance.total === 5460); + })); - // Create new transaction - var t2 = bcoin.mtx().addOutput(w2, 5460); - w1.fund(t2, { rate: 10000, round: true }, function(err) { - assert.ifError(err); - w1.sign(t2, function(err) { - assert.ifError(err); - t2 = t2.toTX(); + it('should sign multiple inputs using different keys', cob(function* () { + var w1 = yield walletdb.create(); + var w2 = yield walletdb.create(); + var to = yield walletdb.create(); + var t1, t2, tx, cost, total, coins1, coins2, left; - assert(t2.verify()); + // Coinbase + t1 = bcoin.mtx() + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460); - assert.equal(t2.getInputValue(), 16380); - // If change < dust and is added to outputs: - // assert.equal(t2.getOutputValue(), 6380); - // If change > dust and is added to fee: - assert.equal(t2.getOutputValue(), 5460); - assert.equal(t2.getFee(), 10920); + t1.addInput(dummyInput); + t1 = t1.toTX(); - // Create new transaction - var t3 = bcoin.mtx().addOutput(w2, 15000); - w1.fund(t3, { rate: 10000, round: true }, function(err) { - assert(err); - assert.equal(err.requiredFunds, 25000); - cb(); - }); - }); - }); - }); - }); - }); - }); + // Coinbase + t2 = bcoin.mtx() + .addOutput(w2.getAddress(), 5460) + .addOutput(w2.getAddress(), 5460) + .addOutput(w2.getAddress(), 5460) + .addOutput(w2.getAddress(), 5460); - it('should fill tx with inputs with accurate fee', function(cb) { - walletdb.create({ master: KEY1 }, function(err, w1) { - assert.ifError(err); - walletdb.create({ master: KEY2 }, function(err, w2) { - assert.ifError(err); + t2.addInput(dummyInput); + t2 = t2.toTX(); - // Coinbase - var t1 = bcoin.mtx() - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460); + yield walletdb.addTX(t1); + yield walletdb.addTX(t2); - t1.addInput(dummyInput); - t1 = t1.toTX(); + // Create our tx with an output + tx = bcoin.mtx(); + tx.addOutput(to.getAddress(), 5460); - walletdb.addTX(t1, function(err) { - assert.ifError(err); + cost = tx.getOutputValue(); + total = cost * constants.tx.MIN_FEE; - // Create new transaction - var t2 = bcoin.mtx().addOutput(w2, 5460); - w1.fund(t2, { rate: 10000 }, function(err) { - assert.ifError(err); - w1.sign(t2, function(err) { - assert.ifError(err); - t2 = t2.toTX(); - assert(t2.verify()); + coins1 = yield w1.getCoins(); + coins2 = yield w2.getCoins(); - assert.equal(t2.getInputValue(), 16380); + // Add dummy output (for `left`) to calculate maximum TX size + tx.addOutput(w1.getAddress(), 0); - // Should now have a change output: - assert.equal(t2.getOutputValue(), 11130); + // Add our unspent inputs to sign + tx.addInput(coins1[0]); + tx.addInput(coins1[1]); + tx.addInput(coins2[0]); - assert.equal(t2.getFee(), 5250); + left = tx.getInputValue() - total; + if (left < constants.tx.DUST_THRESHOLD) { + tx.outputs[tx.outputs.length - 2].value += left; + left = 0; + } + if (left === 0) + tx.outputs.pop(); + else + tx.outputs[tx.outputs.length - 1].value = left; - assert.equal(t2.getWeight(), 2084); - assert.equal(t2.getBaseSize(), 521); - assert.equal(t2.getSize(), 521); - assert.equal(t2.getVirtualSize(), 521); + // Sign transaction + total = yield w1.sign(tx); + assert.equal(total, 2); - var balance; - w2.once('balance', function(b) { - balance = b; - }); + total = yield w2.sign(tx); + assert.equal(total, 1); - // Create new transaction - walletdb.addTX(t2, function(err) { - assert.ifError(err); - var t3 = bcoin.mtx().addOutput(w2, 15000); - w1.fund(t3, { rate: 10000 }, function(err) { - assert(err); - assert(balance); - assert(balance.total === 5460); - cb(); - }); - }); - }); - }); - }); - }); - }); - }); + // Verify + assert.equal(tx.verify(), true); - it('should sign multiple inputs using different keys', function(cb) { - walletdb.create(function(err, w1) { - assert.ifError(err); - walletdb.create(function(err, w2) { - assert.ifError(err); - walletdb.create(function(err, to) { - assert.ifError(err); + // Sign transaction using `inputs` and `off` params. + tx.inputs.length = 0; + tx.addInput(coins1[1]); + tx.addInput(coins1[2]); + tx.addInput(coins2[1]); - // Coinbase - var t1 = bcoin.mtx() - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460); + total = yield w1.sign(tx); + assert.equal(total, 2); - t1.addInput(dummyInput); - t1 = t1.toTX(); + total = yield w2.sign(tx); + assert.equal(total, 1); - // Coinbase - var t2 = bcoin.mtx() - .addOutput(w2, 5460) - .addOutput(w2, 5460) - .addOutput(w2, 5460) - .addOutput(w2, 5460); + // Verify + assert.equal(tx.verify(), true); + })); - t2.addInput(dummyInput); - t2 = t2.toTX(); - - walletdb.addTX(t1, function(err) { - assert.ifError(err); - walletdb.addTX(t2, function(err) { - assert.ifError(err); - - // Create our tx with an output - var tx = bcoin.mtx(); - tx.addOutput(to, 5460); - - var cost = tx.getOutputValue(); - var total = cost * constants.tx.MIN_FEE; - - w1.getCoins(function(err, coins1) { - assert.ifError(err); - w2.getCoins(function(err, coins2) { - assert.ifError(err); - - // Add dummy output (for `left`) to calculate maximum TX size - tx.addOutput(w1, 0); - - // Add our unspent inputs to sign - tx.addInput(coins1[0]); - tx.addInput(coins1[1]); - tx.addInput(coins2[0]); - - var left = tx.getInputValue() - total; - if (left < constants.tx.DUST_THRESHOLD) { - tx.outputs[tx.outputs.length - 2].value += left; - left = 0; - } - if (left === 0) - tx.outputs.pop(); - else - tx.outputs[tx.outputs.length - 1].value = left; - - // Sign transaction - w1.sign(tx, function(err, total) { - assert.ifError(err); - assert.equal(total, 2); - w2.sign(tx, function(err, total) { - assert.ifError(err); - assert.equal(total, 1); - - // Verify - assert.equal(tx.verify(), true); - - // Sign transaction using `inputs` and `off` params. - tx.inputs.length = 0; - tx.addInput(coins1[1]); - tx.addInput(coins1[2]); - tx.addInput(coins2[1]); - w1.sign(tx, function(err, total) { - assert.ifError(err); - assert.equal(total, 2); - w2.sign(tx, function(err, total) { - assert.ifError(err); - assert.equal(total, 1); - - // Verify - assert.equal(tx.verify(), true); - - cb(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - - function multisig(witness, bullshitNesting, cb) { + var multisig = co(function* multisig(witness, bullshitNesting, cb) { var flags = bcoin.constants.flags.STANDARD_VERIFY_FLAGS; + var options, w1, w2, w3, receive, b58, addr, paddr, utx, send, change; + + var rec = bullshitNesting ? 'nested' : 'receive'; + var depth = bullshitNesting ? 'nestedDepth' : 'receiveDepth'; if (witness) flags |= bcoin.constants.flags.VERIFY_WITNESS; // Create 3 2-of-3 wallets with our pubkeys as "shared keys" - var options = { + options = { witness: witness, type: 'multisig', m: 2, n: 3 }; - var w1, w2, w3, receive; + w1 = yield walletdb.create(options); + w2 = yield walletdb.create(options); + w3 = yield walletdb.create(options); + receive = yield walletdb.create(); - utils.serial([ - function(next) { - walletdb.create(options, function(err, w1_) { - assert.ifError(err); - w1 = w1_; - next(); - }); - }, - function(next) { - walletdb.create(options, function(err, w2_) { - assert.ifError(err); - w2 = w2_; - next(); - }); - }, - function(next) { - walletdb.create(options, function(err, w3_) { - assert.ifError(err); - w3 = w3_; - next(); - }); - }, - function(next) { - walletdb.create(function(err, receive_) { - assert.ifError(err); - receive = receive_; - next(); - }); - } - ], function(err) { - assert.ifError(err); + yield w1.addKey(w2.accountKey); + yield w1.addKey(w3.accountKey); + yield w2.addKey(w1.accountKey); + yield w2.addKey(w3.accountKey); + yield w3.addKey(w1.accountKey); + yield w3.addKey(w2.accountKey); - utils.serial([ - w1.addKey.bind(w1, w2.accountKey), - w1.addKey.bind(w1, w3.accountKey), - w2.addKey.bind(w2, w1.accountKey), - w2.addKey.bind(w2, w3.accountKey), - w3.addKey.bind(w3, w1.accountKey), - w3.addKey.bind(w3, w2.accountKey) - ], function(err) { - assert.ifError(err); + // Our p2sh address + b58 = w1[rec].getAddress('base58'); + addr = bcoin.address.fromBase58(b58); - // w3 = bcoin.wallet.fromJSON(w3.toJSON()); + if (witness) { + if (bullshitNesting) + assert.equal(addr.type, scriptTypes.SCRIPTHASH); + else + assert.equal(addr.type, scriptTypes.WITNESSSCRIPTHASH); + } else { + assert.equal(addr.type, scriptTypes.SCRIPTHASH); + } - // Our p2sh address - var addr = w1.getAddress('base58'); + assert.equal(w1[rec].getAddress('base58'), b58); + assert.equal(w2[rec].getAddress('base58'), b58); + assert.equal(w3[rec].getAddress('base58'), b58); - var ad = bcoin.address.fromBase58(addr); + paddr = w1.getNestedAddress('base58'); + assert.equal(w1.getNestedAddress('base58'), paddr); + assert.equal(w2.getNestedAddress('base58'), paddr); + assert.equal(w3.getNestedAddress('base58'), paddr); - if (witness) - assert(ad.type === scriptTypes.WITNESSSCRIPTHASH); - else - assert(ad.type === scriptTypes.SCRIPTHASH); + // Add a shared unspent transaction to our wallets + utx = bcoin.mtx(); + if (bullshitNesting) + utx.addOutput({ address: paddr, value: 5460 * 10 }); + else + utx.addOutput({ address: addr, value: 5460 * 10 }); - assert.equal(w1.getAddress('base58'), addr); - assert.equal(w2.getAddress('base58'), addr); - assert.equal(w3.getAddress('base58'), addr); + utx.addInput(dummyInput); + utx = utx.toTX(); - var paddr = w1.getProgramAddress('base58'); - assert.equal(w1.getProgramAddress('base58'), paddr); - assert.equal(w2.getProgramAddress('base58'), paddr); - assert.equal(w3.getProgramAddress('base58'), paddr); + // Simulate a confirmation + utx.ps = 0; + utx.ts = 1; + utx.height = 1; - // Add a shared unspent transaction to our wallets - var utx = bcoin.mtx(); - if (bullshitNesting) - utx.addOutput({ address: paddr, value: 5460 * 10 }); - else - utx.addOutput({ address: addr, value: 5460 * 10 }); + assert.equal(w1[depth], 1); - utx.addInput(dummyInput); - utx = utx.toTX(); + yield walletdb.addTX(utx); + yield walletdb.addTX(utx); + yield walletdb.addTX(utx); - // Simulate a confirmation - utx.ps = 0; - utx.ts = 1; - utx.height = 1; + assert.equal(w1[depth], 2); - assert.equal(w1.receiveDepth, 1); + assert.equal(w1.changeDepth, 1); - walletdb.addTX(utx, function(err) { - assert.ifError(err); - walletdb.addTX(utx, function(err) { - assert.ifError(err); - walletdb.addTX(utx, function(err) { - assert.ifError(err); + assert(w1[rec].getAddress('base58') !== b58); + b58 = w1[rec].getAddress('base58'); + assert.equal(w1[rec].getAddress('base58'), b58); + assert.equal(w2[rec].getAddress('base58'), b58); + assert.equal(w3[rec].getAddress('base58'), b58); - assert.equal(w1.receiveDepth, 2); - assert.equal(w1.changeDepth, 1); + // Create a tx requiring 2 signatures + send = bcoin.mtx(); + send.addOutput({ address: receive.getAddress(), value: 5460 }); + assert(!send.verify(flags)); + yield w1.fund(send, { rate: 10000, round: true }); - assert(w1.getAddress('base58') !== addr); - addr = w1.getAddress('base58'); - assert.equal(w1.getAddress('base58'), addr); - assert.equal(w2.getAddress('base58'), addr); - assert.equal(w3.getAddress('base58'), addr); + yield w1.sign(send); - // Create a tx requiring 2 signatures - var send = bcoin.mtx(); - send.addOutput({ address: receive.getAddress(), value: 5460 }); - assert(!send.verify(flags)); - w1.fund(send, { rate: 10000, round: true }, function(err) { - assert.ifError(err); + assert(!send.verify(flags)); - w1.sign(send, function(err) { - assert.ifError(err); + yield w2.sign(send); - assert(!send.verify(flags)); - w2.sign(send, function(err) { - assert.ifError(err); + send = send.toTX(); + assert(send.verify(flags)); - send = send.toTX(); - assert(send.verify(flags)); + assert.equal(w1.changeDepth, 1); - assert.equal(w1.changeDepth, 1); - var change = w1.changeAddress.getAddress('base58'); - assert.equal(w1.changeAddress.getAddress('base58'), change); - assert.equal(w2.changeAddress.getAddress('base58'), change); - assert.equal(w3.changeAddress.getAddress('base58'), change); + change = w1.change.getAddress('base58'); + assert.equal(w1.change.getAddress('base58'), change); + assert.equal(w2.change.getAddress('base58'), change); + assert.equal(w3.change.getAddress('base58'), change); - // Simulate a confirmation - send.ps = 0; - send.ts = 1; - send.height = 1; + // Simulate a confirmation + send.ps = 0; + send.ts = 1; + send.height = 1; - walletdb.addTX(send, function(err) { - assert.ifError(err); - walletdb.addTX(send, function(err) { - assert.ifError(err); - walletdb.addTX(send, function(err) { - assert.ifError(err); + yield walletdb.addTX(send); + yield walletdb.addTX(send); + yield walletdb.addTX(send); - assert.equal(w1.receiveDepth, 2); - assert.equal(w1.changeDepth, 2); + assert.equal(w1[depth], 2); + assert.equal(w1.changeDepth, 2); - assert(w1.getAddress('base58') === addr); - assert(w1.changeAddress.getAddress('base58') !== change); - change = w1.changeAddress.getAddress('base58'); - assert.equal(w1.changeAddress.getAddress('base58'), change); - assert.equal(w2.changeAddress.getAddress('base58'), change); - assert.equal(w3.changeAddress.getAddress('base58'), change); + assert(w1[rec].getAddress('base58') === b58); + assert(w1.change.getAddress('base58') !== change); + change = w1.change.getAddress('base58'); + assert.equal(w1.change.getAddress('base58'), change); + assert.equal(w2.change.getAddress('base58'), change); + assert.equal(w3.change.getAddress('base58'), change); - if (witness) { - send.inputs[0].witness.items[2] = new Buffer([]); - } else { - send.inputs[0].script.code[2] = new bcoin.opcode(0); - send.inputs[0].script.compile(); - } + if (witness) { + send.inputs[0].witness.set(2, 0); + } else { + send.inputs[0].script.set(2, 0); + send.inputs[0].script.compile(); + } - assert(!send.verify(flags)); - assert.equal(send.getFee(), 10000); - - // w3 = bcoin.wallet.fromJSON(w3.toJSON()); - // assert.equal(w3.receiveDepth, 2); - // assert.equal(w3.changeDepth, 2); - //assert.equal(w3.getAddress('base58'), addr); - //assert.equal(w3.changeAddress.getAddress('base58'), change); - - cb(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); - } - - it('should verify 2-of-3 scripthash tx', function(cb) { - multisig(false, false, cb); + assert(!send.verify(flags)); + assert.equal(send.getFee(), 10000); }); - it('should verify 2-of-3 witnessscripthash tx', function(cb) { - multisig(true, false, cb); - }); + it('should verify 2-of-3 scripthash tx', cob(function* () { + yield multisig(false, false); + })); - it('should verify 2-of-3 witnessscripthash tx with bullshit nesting', function(cb) { - multisig(true, true, cb); - }); + it('should verify 2-of-3 witnessscripthash tx', cob(function* () { + yield multisig(true, false); + })); - it('should fill tx with account 1', function(cb) { - walletdb.create({}, function(err, w1) { - assert.ifError(err); - walletdb.create({}, function(err, w2) { - assert.ifError(err); - w1.createAccount({ name: 'foo' }, function(err, account) { - assert.ifError(err); - assert.equal(account.name, 'foo'); - assert.equal(account.accountIndex, 1); - w1.getAccount('foo', function(err, account) { - assert.ifError(err); - assert.equal(account.name, 'foo'); - assert.equal(account.accountIndex, 1); + it('should verify 2-of-3 witnessscripthash tx with bullshit nesting', cob(function* () { + yield multisig(true, true); + })); - // Coinbase - var t1 = bcoin.mtx() - .addOutput(account.receiveAddress, 5460) - .addOutput(account.receiveAddress, 5460) - .addOutput(account.receiveAddress, 5460) - .addOutput(account.receiveAddress, 5460); + it('should fill tx with account 1', cob(function* () { + var w1 = yield walletdb.create(); + var w2 = yield walletdb.create(); + var account, accounts, rec, t1, t2, t3, err; - t1.addInput(dummyInput); - t1 = t1.toTX(); + account = yield w1.createAccount({ name: 'foo' }); + assert.equal(account.name, 'foo'); + assert.equal(account.accountIndex, 1); - walletdb.addTX(t1, function(err) { - assert.ifError(err); + account = yield w1.getAccount('foo'); + assert.equal(account.name, 'foo'); + assert.equal(account.accountIndex, 1); + rec = account.receive; - // Create new transaction - var t2 = bcoin.mtx().addOutput(w2, 5460); - w1.fund(t2, { rate: 10000, round: true }, function(err) { - assert.ifError(err); - w1.sign(t2, function(err) { - assert.ifError(err); + // Coinbase + t1 = bcoin.mtx() + .addOutput(rec.getAddress(), 5460) + .addOutput(rec.getAddress(), 5460) + .addOutput(rec.getAddress(), 5460) + .addOutput(rec.getAddress(), 5460); - assert(t2.verify()); + t1.addInput(dummyInput); + t1 = t1.toTX(); - assert.equal(t2.getInputValue(), 16380); - // If change < dust and is added to outputs: - // assert.equal(t2.getOutputValue(), 6380); - // If change > dust and is added to fee: - assert.equal(t2.getOutputValue(), 5460); - assert.equal(t2.getFee(), 10920); + yield walletdb.addTX(t1); - // Create new transaction - var t3 = bcoin.mtx().addOutput(w2, 15000); - w1.fund(t3, { rate: 10000, round: true }, function(err) { - assert(err); - assert.equal(err.requiredFunds, 25000); - w1.getAccounts(function(err, accounts) { - assert.ifError(err); - assert.deepEqual(accounts, ['default', 'foo']); - cb(); - }); - }); - }); - }); - }); - }); - }); - }); - }); - }); + // Create new transaction + t2 = bcoin.mtx().addOutput(w2.getAddress(), 5460); + yield w1.fund(t2, { rate: 10000, round: true }); + yield w1.sign(t2); - it('should fail to fill tx with account 1', function(cb) { - walletdb.create({}, function(err, w1) { - assert.ifError(err); - lastW = w1; - w1.createAccount({ name: 'foo' }, function(err, acc) { - assert.ifError(err); - assert.equal(acc.name, 'foo'); - assert.equal(acc.accountIndex, 1); - w1.getAccount('foo', function(err, account) { - assert.ifError(err); - assert.equal(account.name, 'foo'); - assert.equal(account.accountIndex, 1); - // assert(account !== w1.account); - // assert(account !== acc); - assert(account.accountKey.xpubkey === acc.accountKey.xpubkey); - assert(w1.account.accountIndex === 0); - assert(account.receiveAddress.getAddress('base58') !== w1.account.receiveAddress.getAddress('base58')); - assert(w1.getAddress('base58') === w1.account.receiveAddress.getAddress('base58')); + assert(t2.verify()); - // Coinbase - var t1 = bcoin.mtx() - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(account.receiveAddress, 5460); + assert.equal(t2.getInputValue(), 16380); + assert.equal(t2.getOutputValue(), 5460); + assert.equal(t2.getFee(), 10920); - t1.addInput(dummyInput); - t1 = t1.toTX(); + // Create new transaction + t3 = bcoin.mtx().addOutput(w2.getAddress(), 15000); - walletdb.addTX(t1, function(err) { - assert.ifError(err); + try { + yield w1.fund(t3, { rate: 10000, round: true }); + } catch (e) { + err = e; + } - // Should fill from `foo` and fail - var t2 = bcoin.mtx().addOutput(w1, 5460); - w1.fund(t2, { rate: 10000, round: true, account: 'foo' }, function(err) { - assert(err); - // Should fill from whole wallet and succeed - var t2 = bcoin.mtx().addOutput(w1, 5460); - w1.fund(t2, { rate: 10000, round: true }, function(err) { - assert.ifError(err); + assert(err); + assert.equal(err.requiredFunds, 25000); - // Coinbase - var t1 = bcoin.mtx() - .addOutput(account.receiveAddress, 5460) - .addOutput(account.receiveAddress, 5460) - .addOutput(account.receiveAddress, 5460); + accounts = yield w1.getAccounts(); + assert.deepEqual(accounts, ['default', 'foo']); + })); - t1.ps = 0xdeadbeef; - t1.addInput(dummyInput); - t1 = t1.toTX(); + it('should fail to fill tx with account 1', cob(function* () { + var w = yield walletdb.create(); + var acc, account, t1, t2, err; - walletdb.addTX(t1, function(err) { - assert.ifError(err); - var t2 = bcoin.mtx().addOutput(w1, 5460); - // Should fill from `foo` and succeed - w1.fund(t2, { rate: 10000, round: true, account: 'foo' }, function(err) { - assert.ifError(err); - cb(); - }); - }); - }); - }); - }); - }); - }); - }); - }); + wallet = w; - it('should fill tx with inputs when encrypted', function(cb) { - walletdb.create({ passphrase: 'foo' }, function(err, w1) { - assert.ifError(err); - w1.master.stop(); - w1.master.key = null; + acc = yield w.createAccount({ name: 'foo' }); + assert.equal(acc.name, 'foo'); + assert.equal(acc.accountIndex, 1); - // Coinbase - var t1 = bcoin.mtx() - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460); + account = yield w.getAccount('foo'); + assert.equal(account.name, 'foo'); + assert.equal(account.accountIndex, 1); + assert(account.accountKey.xpubkey === acc.accountKey.xpubkey); + assert(w.account.accountIndex === 0); - t1.addInput(dummyInput); - t1 = t1.toTX(); + assert.notEqual( + account.receive.getAddress('base58'), + w.account.receive.getAddress('base58')); - walletdb.addTX(t1, function(err) { - assert.ifError(err); + assert.equal(w.getAddress('base58'), + w.account.receive.getAddress('base58')); - // Create new transaction - var t2 = bcoin.mtx().addOutput(w1, 5460); - w1.fund(t2, { rate: 10000, round: true }, function(err) { - assert.ifError(err); - // Should fail - w1.sign(t2, 'bar', function(err) { - assert(err); - assert(!t2.verify()); - // Should succeed - w1.sign(t2, 'foo', function(err) { - assert.ifError(err); - assert(t2.verify()); - cb(); - }); - }); - }); - }); - }); - }); + // Coinbase + t1 = bcoin.mtx() + .addOutput(w.getAddress(), 5460) + .addOutput(w.getAddress(), 5460) + .addOutput(w.getAddress(), 5460) + .addOutput(account.receive.getAddress(), 5460); - it('should fill tx with inputs with subtract fee', function(cb) { - walletdb.create(function(err, w1) { - assert.ifError(err); - walletdb.create(function(err, w2) { - assert.ifError(err); + t1.addInput(dummyInput); + t1 = t1.toTX(); - // Coinbase - var t1 = bcoin.mtx() - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460); + yield walletdb.addTX(t1); - t1.addInput(dummyInput); - t1 = t1.toTX(); + // Should fill from `foo` and fail + t2 = bcoin.mtx().addOutput(w.getAddress(), 5460); + try { + yield w.fund(t2, { rate: 10000, round: true, account: 'foo' }); + } catch (e) { + err = e; + } + assert(err); - walletdb.addTX(t1, function(err) { - assert.ifError(err); + // Should fill from whole wallet and succeed + t2 = bcoin.mtx().addOutput(w.getAddress(), 5460); + yield w.fund(t2, { rate: 10000, round: true }); - // Create new transaction - var t2 = bcoin.mtx().addOutput(w2, 21840); - w1.fund(t2, { rate: 10000, round: true, subtractFee: true }, function(err) { - assert.ifError(err); - w1.sign(t2, function(err) { - assert.ifError(err); + // Coinbase + t1 = bcoin.mtx() + .addOutput(account.receive.getAddress(), 5460) + .addOutput(account.receive.getAddress(), 5460) + .addOutput(account.receive.getAddress(), 5460); - assert(t2.verify()); + t1.ps = 0xdeadbeef; + t1.addInput(dummyInput); + t1 = t1.toTX(); - assert.equal(t2.getInputValue(), 5460 * 4); - assert.equal(t2.getOutputValue(), 21840 - 10000); - assert.equal(t2.getFee(), 10000); + yield walletdb.addTX(t1); - cb(); - }); - }); - }); - }); - }); - }); + t2 = bcoin.mtx().addOutput(w.getAddress(), 5460); + // Should fill from `foo` and succeed + yield w.fund(t2, { rate: 10000, round: true, account: 'foo' }); + })); - it('should fill tx with inputs with subtract fee with create tx', function(cb) { - walletdb.create(function(err, w1) { - assert.ifError(err); - walletdb.create(function(err, w2) { - assert.ifError(err); + it('should fill tx with inputs when encrypted', cob(function* () { + var w = yield walletdb.create({ passphrase: 'foo' }); + var t1, t2, err; - // Coinbase - var t1 = bcoin.mtx() - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460) - .addOutput(w1, 5460); + w.master.stop(); + w.master.key = null; - t1.addInput(dummyInput); - t1 = t1.toTX(); + // Coinbase + t1 = bcoin.mtx() + .addOutput(w.getAddress(), 5460) + .addOutput(w.getAddress(), 5460) + .addOutput(w.getAddress(), 5460) + .addOutput(w.getAddress(), 5460); - walletdb.addTX(t1, function(err) { - assert.ifError(err); + t1.addInput(dummyInput); + t1 = t1.toTX(); - var options = { - subtractFee: true, - rate: 10000, - round: true, - outputs: [{ address: w2.getAddress(), value: 21840 }] - }; + yield walletdb.addTX(t1); - // Create new transaction - w1.createTX(options, function(err, t2) { - assert.ifError(err); - w1.sign(t2, function(err) { - assert.ifError(err); + // Create new transaction + t2 = bcoin.mtx().addOutput(w.getAddress(), 5460); + yield w.fund(t2, { rate: 10000, round: true }); - assert(t2.verify()); + // Should fail + try { + yield w.sign(t2, { passphrase: 'bar' }); + } catch (e) { + err = e; + } - assert.equal(t2.getInputValue(), 5460 * 4); - assert.equal(t2.getOutputValue(), 21840 - 10000); - assert.equal(t2.getFee(), 10000); + assert(err); + assert(!t2.verify()); - cb(); - }); - }); - }); - }); - }); - }); + // Should succeed + yield w.sign(t2, { passphrase: 'foo' }); + assert(t2.verify()); + })); - it('should get range of txs', function(cb) { - var w1 = lastW; - w1.getRange({ start: 0xdeadbeef - 1000 }, function(err, txs) { - if (err) - return callback(err); - assert.equal(txs.length, 1); - cb(); - }); - }); + it('should fill tx with inputs with subtract fee', cob(function* () { + var w1 = yield walletdb.create(); + var w2 = yield walletdb.create(); + var t1, t2; - it('should get range of txs from account', function(cb) { - var w1 = lastW; - w1.getRange('foo', { start: 0xdeadbeef - 1000 }, function(err, txs) { - if (err) - return callback(err); - assert.equal(txs.length, 1); - cb(); - }); - }); + // Coinbase + t1 = bcoin.mtx() + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460); - it('should not get range of txs from non-existent account', function(cb) { - var w1 = lastW; - w1.getRange('bad', { start: 0xdeadbeef - 1000 }, function(err, txs) { - assert(err); - assert.equal(err.message, 'Account not found.'); - cb(); - }); - }); + t1.addInput(dummyInput); + t1 = t1.toTX(); - it('should get account balance', function(cb) { - var w1 = lastW; - w1.getBalance('foo', function(err, balance) { - assert.ifError(err); - assert.equal(balance.total, 21840); - cb(); - }); - }); + yield walletdb.addTX(t1); - it('should import key', function(cb) { + // Create new transaction + t2 = bcoin.mtx().addOutput(w2.getAddress(), 21840); + yield w1.fund(t2, { rate: 10000, round: true, subtractFee: true }); + yield w1.sign(t2); + + assert(t2.verify()); + + assert.equal(t2.getInputValue(), 5460 * 4); + assert.equal(t2.getOutputValue(), 21840 - 10000); + assert.equal(t2.getFee(), 10000); + })); + + it('should fill tx with inputs with subtract fee with create tx', cob(function* () { + var w1 = yield walletdb.create(); + var w2 = yield walletdb.create(); + var options, t1, t2; + + // Coinbase + t1 = bcoin.mtx() + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460) + .addOutput(w1.getAddress(), 5460); + + t1.addInput(dummyInput); + t1 = t1.toTX(); + + yield walletdb.addTX(t1); + + options = { + subtractFee: true, + rate: 10000, + round: true, + outputs: [{ address: w2.getAddress(), value: 21840 }] + }; + + // Create new transaction + t2 = yield w1.createTX(options); + yield w1.sign(t2); + + assert(t2.verify()); + + assert.equal(t2.getInputValue(), 5460 * 4); + assert.equal(t2.getOutputValue(), 21840 - 10000); + assert.equal(t2.getFee(), 10000); + })); + + it('should get range of txs', cob(function* () { + var w = wallet; + var txs = yield w.getRange({ start: 0xdeadbeef - 1000 }); + assert.equal(txs.length, 1); + })); + + it('should get range of txs from account', cob(function* () { + var w = wallet; + var txs = yield w.getRange('foo', { start: 0xdeadbeef - 1000 }); + assert.equal(txs.length, 1); + })); + + it('should not get range of txs from non-existent account', cob(function* () { + var w = wallet; + var txs, err; + + try { + txs = yield w.getRange('bad', { start: 0xdeadbeef - 1000 }); + } catch (e) { + err = e; + } + + assert(err); + assert.equal(err.message, 'Account not found.'); + })); + + it('should get account balance', cob(function* () { + var w = wallet; + var balance = yield w.getBalance('foo'); + assert.equal(balance.total, 21840); + })); + + it('should import privkey', cob(function* () { var key = bcoin.keyring.generate(); - walletdb.create({ passphrase: 'test' }, function(err, w1) { - assert.ifError(err); - w1.importKey('default', key, 'test', function(err) { - assert.ifError(err); - w1.getKeyRing(key.getHash('hex'), function(err, k) { - if (err) - return callback(err); + var w = yield walletdb.create({ passphrase: 'test' }); + var options, k, t1, t2, tx; - assert.equal(k.getHash('hex'), key.getHash('hex')); + yield w.importKey('default', key, 'test'); - // Coinbase - var t1 = bcoin.mtx() - .addOutput(key.getAddress(), 5460) - .addOutput(key.getAddress(), 5460) - .addOutput(key.getAddress(), 5460) - .addOutput(key.getAddress(), 5460); + k = yield w.getKey(key.getHash('hex')); - t1.addInput(dummyInput); - t1 = t1.toTX(); + assert.equal(k.getHash('hex'), key.getHash('hex')); - walletdb.addTX(t1, function(err) { - assert.ifError(err); + // Coinbase + t1 = bcoin.mtx() + .addOutput(key.getAddress(), 5460) + .addOutput(key.getAddress(), 5460) + .addOutput(key.getAddress(), 5460) + .addOutput(key.getAddress(), 5460); - w1.getTX(t1.hash('hex'), function(err, tx) { - assert.ifError(err); - assert(tx); - assert.equal(t1.hash('hex'), tx.hash('hex')); + t1.addInput(dummyInput); + t1 = t1.toTX(); - var options = { - rate: 10000, - round: true, - outputs: [{ address: w1.getAddress(), value: 7000 }] - }; + yield walletdb.addTX(t1); - // Create new transaction - w1.createTX(options, function(err, t2) { - assert.ifError(err); - w1.sign(t2, function(err) { - assert.ifError(err); - assert(t2.verify()); - assert(t2.inputs[0].prevout.hash === tx.hash('hex')); - cb(); - }); - }); - }); - }); - }); - }); - }); - }); + tx = yield w.getTX(t1.hash('hex')); + assert(tx); + assert.equal(t1.hash('hex'), tx.hash('hex')); - it('should cleanup', function(cb) { - walletdb.dump(function(err, records) { - assert.ifError(err); - constants.tx.COINBASE_MATURITY = 100; - cb(); - }); - }); + options = { + rate: 10000, + round: true, + outputs: [{ address: w.getAddress(), value: 7000 }] + }; + + // Create new transaction + t2 = yield w.createTX(options); + yield w.sign(t2); + assert(t2.verify()); + assert(t2.inputs[0].prevout.hash === tx.hash('hex')); + + ewallet = w; + ekey = key; + })); + + it('should import pubkey', cob(function* () { + var priv = bcoin.keyring.generate(); + var key = new bcoin.keyring(priv.publicKey); + var w = yield walletdb.create({ watchOnly: true }); + var options, k, t1, t2, tx; + + yield w.importKey('default', key); + + k = yield w.getPath(key.getHash('hex')); + + assert.equal(k.hash, key.getHash('hex')); + + k = yield w.getKey(key.getHash('hex')); + assert(k); + })); + + it('should import address', cob(function* () { + var key = bcoin.keyring.generate(); + var w = yield walletdb.create({ watchOnly: true }); + var options, k, t1, t2, tx; + + yield w.importAddress('default', key.getAddress()); + + k = yield w.getPath(key.getHash('hex')); + + assert.equal(k.hash, key.getHash('hex')); + + k = yield w.getKey(key.getHash('hex')); + assert(!k); + })); + + it('should get details', cob(function* () { + var w = wallet; + var txs = yield w.getRange('foo', { start: 0xdeadbeef - 1000 }); + var details = yield w.toDetails(txs); + assert.equal(details[0].toJSON().outputs[0].path.name, 'foo'); + })); + + it('should rename wallet', cob(function* () { + var w = wallet; + yield wallet.rename('test'); + var txs = yield w.getRange('foo', { start: 0xdeadbeef - 1000 }); + var details = yield w.toDetails(txs); + assert.equal(details[0].toJSON().id, 'test'); + })); + + it('should handle changed passphrase with encrypted imports', cob(function* () { + var w = ewallet; + var addr = ekey.getAddress(); + var path, d1, d2, k; + + assert(w.master.encrypted); + + path = yield w.getPath(addr); + assert(path); + assert(path.data && path.encrypted); + d1 = path.data; + + yield w.decrypt('test'); + + path = yield w.getPath(addr); + assert(path); + assert(path.data && !path.encrypted); + + k = yield w.getKey(addr); + assert(k); + + yield w.encrypt('foo'); + + path = yield w.getPath(addr); + assert(path); + assert(path.data && path.encrypted); + d2 = path.data; + + assert(!utils.equal(d1, d2)); + + k = yield w.getKey(addr); + assert(!k); + + yield w.unlock('foo'); + k = yield w.getKey(addr); + assert(k); + assert.equal(k.getHash('hex'), addr.getHash('hex')); + })); + + it('should cleanup', cob(function* () { + var records = yield walletdb.dump(); + constants.tx.COINBASE_MATURITY = 100; + })); }); diff --git a/vendor/ip.js b/vendor/ip.js index 21270a65..85ee7f00 100644 --- a/vendor/ip.js +++ b/vendor/ip.js @@ -377,7 +377,7 @@ ip.address = function(name, family) { var os; try { - os = require('o' + 's'); + os = require('os'); } catch (e) { return '127.0.0.1'; }