diff options
-rw-r--r-- | src/Makefile.am | 1 | ||||
-rw-r--r-- | src/rpc/mining.cpp | 38 | ||||
-rw-r--r-- | src/rpc/mining.h | 15 | ||||
-rw-r--r-- | src/validationinterface.cpp | 3 | ||||
-rw-r--r-- | src/validationinterface.h | 3 | ||||
-rw-r--r-- | src/wallet/rpcwallet.cpp | 48 | ||||
-rw-r--r-- | src/wallet/wallet.h | 2 | ||||
-rwxr-xr-x | test/functional/dbcrash.py | 109 | ||||
-rw-r--r-- | test/functional/test_framework/util.py | 5 |
9 files changed, 131 insertions, 93 deletions
diff --git a/src/Makefile.am b/src/Makefile.am index 9b9dd89f68..8ecd391804 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -127,6 +127,7 @@ BITCOIN_CORE_H = \ reverselock.h \ rpc/blockchain.h \ rpc/client.h \ + rpc/mining.h \ rpc/protocol.h \ rpc/server.h \ rpc/register.h \ diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 8c682592c5..e50742f36e 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -18,6 +18,7 @@ #include "policy/fees.h" #include "pow.h" #include "rpc/blockchain.h" +#include "rpc/mining.h" #include "rpc/server.h" #include "txmempool.h" #include "util.h" @@ -141,42 +142,6 @@ UniValue generateBlocks(std::shared_ptr<CReserveScript> coinbaseScript, int nGen return blockHashes; } -UniValue generate(const JSONRPCRequest& request) -{ - if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) - throw std::runtime_error( - "generate nblocks ( maxtries )\n" - "\nMine up to nblocks blocks immediately (before the RPC call returns)\n" - "\nArguments:\n" - "1. nblocks (numeric, required) How many blocks are generated immediately.\n" - "2. maxtries (numeric, optional) How many iterations to try (default = 1000000).\n" - "\nResult:\n" - "[ blockhashes ] (array) hashes of blocks generated\n" - "\nExamples:\n" - "\nGenerate 11 blocks\n" - + HelpExampleCli("generate", "11") - ); - - int nGenerate = request.params[0].get_int(); - uint64_t nMaxTries = 1000000; - if (request.params.size() > 1) { - nMaxTries = request.params[1].get_int(); - } - - std::shared_ptr<CReserveScript> coinbaseScript; - GetMainSignals().ScriptForMining(coinbaseScript); - - // If the keypool is exhausted, no script is returned at all. Catch this. - if (!coinbaseScript) - throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); - - //throw an error if no script was provided - if (coinbaseScript->reserveScript.empty()) - throw JSONRPCError(RPC_INTERNAL_ERROR, "No coinbase script available (mining requires a wallet)"); - - return generateBlocks(coinbaseScript, nGenerate, nMaxTries, true); -} - UniValue generatetoaddress(const JSONRPCRequest& request) { if (request.fHelp || request.params.size() < 2 || request.params.size() > 3) @@ -962,7 +927,6 @@ static const CRPCCommand commands[] = { "mining", "getblocktemplate", &getblocktemplate, true, {"template_request"} }, { "mining", "submitblock", &submitblock, true, {"hexdata","dummy"} }, - { "generating", "generate", &generate, true, {"nblocks","maxtries"} }, { "generating", "generatetoaddress", &generatetoaddress, true, {"nblocks","address","maxtries"} }, { "util", "estimatefee", &estimatefee, true, {"nblocks"} }, diff --git a/src/rpc/mining.h b/src/rpc/mining.h new file mode 100644 index 0000000000..a148d851da --- /dev/null +++ b/src/rpc/mining.h @@ -0,0 +1,15 @@ +// Copyright (c) 2017 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_RPC_MINING_H +#define BITCOIN_RPC_MINING_H + +#include "script/script.h" + +#include <univalue.h> + +/** Generate blocks (mine) */ +UniValue generateBlocks(std::shared_ptr<CReserveScript> coinbaseScript, int nGenerate, uint64_t nMaxTries, bool keepScript); + +#endif diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp index 46d7c9b329..be2f20b863 100644 --- a/src/validationinterface.cpp +++ b/src/validationinterface.cpp @@ -21,12 +21,10 @@ void RegisterValidationInterface(CValidationInterface* pwalletIn) { g_signals.Inventory.connect(boost::bind(&CValidationInterface::Inventory, pwalletIn, _1)); g_signals.Broadcast.connect(boost::bind(&CValidationInterface::ResendWalletTransactions, pwalletIn, _1, _2)); g_signals.BlockChecked.connect(boost::bind(&CValidationInterface::BlockChecked, pwalletIn, _1, _2)); - g_signals.ScriptForMining.connect(boost::bind(&CValidationInterface::GetScriptForMining, pwalletIn, _1)); g_signals.NewPoWValidBlock.connect(boost::bind(&CValidationInterface::NewPoWValidBlock, pwalletIn, _1, _2)); } void UnregisterValidationInterface(CValidationInterface* pwalletIn) { - g_signals.ScriptForMining.disconnect(boost::bind(&CValidationInterface::GetScriptForMining, pwalletIn, _1)); g_signals.BlockChecked.disconnect(boost::bind(&CValidationInterface::BlockChecked, pwalletIn, _1, _2)); g_signals.Broadcast.disconnect(boost::bind(&CValidationInterface::ResendWalletTransactions, pwalletIn, _1, _2)); g_signals.Inventory.disconnect(boost::bind(&CValidationInterface::Inventory, pwalletIn, _1)); @@ -39,7 +37,6 @@ void UnregisterValidationInterface(CValidationInterface* pwalletIn) { } void UnregisterAllValidationInterfaces() { - g_signals.ScriptForMining.disconnect_all_slots(); g_signals.BlockChecked.disconnect_all_slots(); g_signals.Broadcast.disconnect_all_slots(); g_signals.Inventory.disconnect_all_slots(); diff --git a/src/validationinterface.h b/src/validationinterface.h index 460aecf243..17545018df 100644 --- a/src/validationinterface.h +++ b/src/validationinterface.h @@ -40,7 +40,6 @@ protected: virtual void Inventory(const uint256 &hash) {} virtual void ResendWalletTransactions(int64_t nBestBlockTime, CConnman* connman) {} virtual void BlockChecked(const CBlock&, const CValidationState&) {} - virtual void GetScriptForMining(std::shared_ptr<CReserveScript>&) {}; virtual void NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr<const CBlock>& block) {}; friend void ::RegisterValidationInterface(CValidationInterface*); friend void ::UnregisterValidationInterface(CValidationInterface*); @@ -72,8 +71,6 @@ struct CMainSignals { * callback was generated (not necessarily now) */ boost::signals2::signal<void (const CBlock&, const CValidationState&)> BlockChecked; - /** Notifies listeners that a key for mining is required (coinbase) */ - boost::signals2::signal<void (std::shared_ptr<CReserveScript>&)> ScriptForMining; /** * Notifies listeners that a block which builds directly on our current tip * has been received and connected to the headers tree, though not validated yet */ diff --git a/src/wallet/rpcwallet.cpp b/src/wallet/rpcwallet.cpp index 5bbb5088e2..e0c7ab9f0f 100644 --- a/src/wallet/rpcwallet.cpp +++ b/src/wallet/rpcwallet.cpp @@ -15,6 +15,7 @@ #include "policy/fees.h" #include "policy/policy.h" #include "policy/rbf.h" +#include "rpc/mining.h" #include "rpc/server.h" #include "script/sign.h" #include "timedata.h" @@ -2922,6 +2923,51 @@ UniValue bumpfee(const JSONRPCRequest& request) return result; } +UniValue generate(const JSONRPCRequest& request) +{ + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + if (request.fHelp || request.params.size() < 1 || request.params.size() > 2) { + throw std::runtime_error( + "generate nblocks ( maxtries )\n" + "\nMine up to nblocks blocks immediately (before the RPC call returns) to an address in the wallet.\n" + "\nArguments:\n" + "1. nblocks (numeric, required) How many blocks are generated immediately.\n" + "2. maxtries (numeric, optional) How many iterations to try (default = 1000000).\n" + "\nResult:\n" + "[ blockhashes ] (array) hashes of blocks generated\n" + "\nExamples:\n" + "\nGenerate 11 blocks\n" + + HelpExampleCli("generate", "11") + ); + } + + int num_generate = request.params[0].get_int(); + uint64_t max_tries = 1000000; + if (request.params.size() > 1 && !request.params[1].isNull()) { + max_tries = request.params[1].get_int(); + } + + std::shared_ptr<CReserveScript> coinbase_script; + pwallet->GetScriptForMining(coinbase_script); + + // If the keypool is exhausted, no script is returned at all. Catch this. + if (!coinbase_script) { + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); + } + + //throw an error if no script was provided + if (coinbase_script->reserveScript.empty()) { + throw JSONRPCError(RPC_INTERNAL_ERROR, "No coinbase script available"); + } + + return generateBlocks(coinbase_script, num_generate, max_tries, true); +} + extern UniValue abortrescan(const JSONRPCRequest& request); // in rpcdump.cpp extern UniValue dumpprivkey(const JSONRPCRequest& request); // in rpcdump.cpp extern UniValue importprivkey(const JSONRPCRequest& request); @@ -2985,6 +3031,8 @@ static const CRPCCommand commands[] = { "wallet", "walletpassphrasechange", &walletpassphrasechange, true, {"oldpassphrase","newpassphrase"} }, { "wallet", "walletpassphrase", &walletpassphrase, true, {"passphrase","timeout"} }, { "wallet", "removeprunedfunds", &removeprunedfunds, true, {"txid"} }, + + { "generating", "generate", &generate, true, {"nblocks","maxtries"} }, }; void RegisterWalletRPCCommands(CRPCTable &t) diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index a3fd7408a0..4f558adc77 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -1025,7 +1025,7 @@ public: } } - void GetScriptForMining(std::shared_ptr<CReserveScript> &script) override; + void GetScriptForMining(std::shared_ptr<CReserveScript> &script); unsigned int GetKeyPoolSize() { diff --git a/test/functional/dbcrash.py b/test/functional/dbcrash.py index 6f877f8362..8339305f5e 100755 --- a/test/functional/dbcrash.py +++ b/test/functional/dbcrash.py @@ -2,21 +2,7 @@ # Copyright (c) 2017 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. -"""Test recovery from a crash during chainstate writing.""" - -from test_framework.test_framework import BitcoinTestFramework -from test_framework.util import * -from test_framework.script import * -from test_framework.mininode import * -import random -try: - import http.client as httplib -except ImportError: - import httplib -import errno - -''' -Test structure: +"""Test recovery from a crash during chainstate writing. - 4 nodes * node0, node1, and node2 will have different dbcrash ratios, and different @@ -37,11 +23,26 @@ Test structure: * submit block to node * if node crashed on/after submitting: - restart until recovery succeeds - - check that utxo matches node3 using gettxoutsetinfo -''' + - check that utxo matches node3 using gettxoutsetinfo""" -class ChainstateWriteCrashTest(BitcoinTestFramework): +import errno +import http.client +import random +import sys +import time + +from test_framework.mininode import * +from test_framework.script import * +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import * + +HTTP_DISCONNECT_ERRORS = [http.client.CannotSendRequest] +try: + HTTP_DISCONNECT_ERRORS.append(http.client.RemoteDisconnected) +except AttributeError: + pass +class ChainstateWriteCrashTest(BitcoinTestFramework): def __init__(self): super().__init__() self.num_nodes = 4 @@ -50,32 +51,28 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Set -maxmempool=0 to turn off mempool memory sharing with dbcache # Set -rpcservertimeout=900 to reduce socket disconnects in this # long-running test - self.base_args = ["-limitdescendantsize=0", "-maxmempool=0", "-rpcservertimeout=900"] + self.base_args = ["-limitdescendantsize=0", "-maxmempool=0", "-rpcservertimeout=900", "-dbbatchsize=200000"] # Set different crash ratios and cache sizes. Note that not all of # -dbcache goes to pcoinsTip. - self.node0_args = ["-dbcrashratio=8", "-dbcache=4", "-dbbatchsize=200000"] + self.base_args - self.node1_args = ["-dbcrashratio=16", "-dbcache=8", "-dbbatchsize=200000"] + self.base_args - self.node2_args = ["-dbcrashratio=24", "-dbcache=16", "-dbbatchsize=200000"] + self.base_args + self.node0_args = ["-dbcrashratio=8", "-dbcache=4"] + self.base_args + self.node1_args = ["-dbcrashratio=16", "-dbcache=8"] + self.base_args + self.node2_args = ["-dbcrashratio=24", "-dbcache=16"] + self.base_args # Node3 is a normal node with default args, except will mine full blocks self.node3_args = ["-blockmaxweight=4000000"] self.extra_args = [self.node0_args, self.node1_args, self.node2_args, self.node3_args] - # We'll track some test coverage statistics - self.restart_counts = [0, 0, 0] # Track the restarts for nodes 0-2 - self.crashed_on_restart = 0 # Track count of crashes during recovery - def setup_network(self): self.setup_nodes() # Leave them unconnected, we'll use submitblock directly in this test - # Starts up a given node id, waits for the tip to reach the given block - # hash, and calculates the utxo hash. Exceptions on startup should - # indicate node crash (due to -dbcrashratio), in which case we try again. - # Give up after 60 seconds. - # Returns the utxo hash of the given node. def restart_node(self, node_index, expected_tip): + """Start up a given node id, wait for the tip to reach the given block hash, and calculate the utxo hash. + + Exceptions on startup should indicate node crash (due to -dbcrashratio), in which case we try again. Give up + after 60 seconds. Returns the utxo hash of the given node.""" + time_start = time.time() while time.time() - time_start < 60: try: @@ -99,14 +96,23 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # and make sure that recovery happens. raise AssertionError("Unable to successfully restart node %d in allotted time", node_index) - # Try submitting a block to the given node. - # Catch any exceptions that indicate the node has crashed. - # Returns true if the block was submitted successfully; false otherwise. def submit_block_catch_error(self, node_index, block): + """Try submitting a block to the given node. + + Catch any exceptions that indicate the node has crashed. + Returns true if the block was submitted successfully; false otherwise.""" + try: self.nodes[node_index].submitblock(block) return True - except (httplib.CannotSendRequest, httplib.RemoteDisconnected) as e: + except http.client.BadStatusLine as e: + # Prior to 3.5 BadStatusLine('') was raised for a remote disconnect error. + if sys.version_info[0] == 3 and sys.version_info[1] < 5 and e.line == "''": + self.log.debug("node %d submitblock raised exception: %s", node_index, e) + return False + else: + raise + except tuple(HTTP_DISCONNECT_ERRORS) as e: self.log.debug("node %d submitblock raised exception: %s", node_index, e) return False except OSError as e: @@ -118,11 +124,13 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Unexpected exception, raise raise - # Use submitblock to sync node3's chain with the other nodes - # If submitblock fails, restart the node and get the new utxo hash. def sync_node3blocks(self, block_hashes): - # If any nodes crash while updating, we'll compare utxo hashes to - # ensure recovery was successful. + """Use submitblock to sync node3's chain with the other nodes + + If submitblock fails, restart the node and get the new utxo hash. + If any nodes crash while updating, we'll compare utxo hashes to + ensure recovery was successful.""" + node3_utxo_hash = self.nodes[3].gettxoutsetinfo()['hash_serialized_2'] # Retrieve all the blocks from node3 @@ -161,9 +169,10 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): self.log.debug("Checking txoutsetinfo matches for node %d", i) assert_equal(nodei_utxo_hash, node3_utxo_hash) - # Verify that the utxo hash of each node matches node3. - # Restart any nodes that crash while querying. def verify_utxo_hash(self): + """Verify that the utxo hash of each node matches node3. + + Restart any nodes that crash while querying.""" node3_utxo_hash = self.nodes[3].gettxoutsetinfo()['hash_serialized_2'] self.log.info("Verifying utxo hash matches for all nodes") @@ -175,9 +184,8 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): nodei_utxo_hash = self.restart_node(i, self.nodes[3].getbestblockhash()) assert_equal(nodei_utxo_hash, node3_utxo_hash) - def generate_small_transactions(self, node, count, utxo_list): - FEE = 1000 # TODO: replace this with node relay fee based calculation + FEE = 1000 # TODO: replace this with node relay fee based calculation num_transactions = 0 random.shuffle(utxo_list) while len(utxo_list) >= 2 and num_transactions < count: @@ -186,8 +194,8 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): for i in range(2): utxo = utxo_list.pop() tx.vin.append(CTxIn(COutPoint(int(utxo['txid'], 16), utxo['vout']))) - input_amount += int(utxo['amount']*COIN) - output_amount = (input_amount - FEE)//3 + input_amount += int(utxo['amount'] * COIN) + output_amount = (input_amount - FEE) // 3 if output_amount <= 0: # Sanity check -- if we chose inputs that are too small, skip @@ -202,6 +210,9 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): num_transactions += 1 def run_test(self): + # Track test coverage statistics + self.restart_counts = [0, 0, 0] # Track the restarts for nodes 0-2 + self.crashed_on_restart = 0 # Track count of crashes during recovery # Start by creating a lot of utxos on node3 initial_height = self.nodes[3].getblockcount() @@ -210,7 +221,7 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): # Sync these blocks with the other nodes block_hashes_to_sync = [] - for height in range(initial_height+1, self.nodes[3].getblockcount()+1): + for height in range(initial_height + 1, self.nodes[3].getblockcount() + 1): block_hashes_to_sync.append(self.nodes[3].getblockhash(height)) self.log.debug("Syncing %d blocks with other nodes", len(block_hashes_to_sync)) @@ -233,13 +244,15 @@ class ChainstateWriteCrashTest(BitcoinTestFramework): if random_height > starting_tip_height: # Randomly reorg from this point with some probability (1/4 for # tip, 1/5 for tip-1, ...) - if random.random() < 1.0/(current_height + 4 - random_height): + if random.random() < 1.0 / (current_height + 4 - random_height): self.log.debug("Invalidating block at height %d", random_height) self.nodes[3].invalidateblock(self.nodes[3].getblockhash(random_height)) # Now generate new blocks until we pass the old tip height self.log.debug("Mining longer tip") - block_hashes = self.nodes[3].generate(current_height+1-self.nodes[3].getblockcount()) + block_hashes = [] + while current_height + 1 > self.nodes[3].getblockcount(): + block_hashes.extend(self.nodes[3].generate(min(10, current_height + 1 - self.nodes[3].getblockcount()))) self.log.debug("Syncing %d new blocks...", len(block_hashes)) self.sync_node3blocks(block_hashes) utxo_list = self.nodes[3].listunspent() diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 3c918b48fb..8a2d8de50e 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -412,7 +412,10 @@ def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants): # Helper to create at least "count" utxos # Pass in a fee that is sufficient for relay and mining new transactions. def create_confirmed_utxos(fee, node, count): - node.generate(int(0.5 * count) + 101) + to_generate = int(0.5 * count) + 101 + while to_generate > 0: + node.generate(min(25, to_generate)) + to_generate -= 25 utxos = node.listunspent() iterations = count - len(utxos) addr1 = node.getnewaddress() |