diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,4 +31,6 @@ add_subdirectory(src) add_subdirectory(test) + +add_subdirectory(contrib/devtools) add_subdirectory(doc) diff --git a/contrib/devtools/CMakeLists.txt b/contrib/devtools/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/contrib/devtools/CMakeLists.txt @@ -0,0 +1,3 @@ +# Copyright (c) 2019 The Bitcoin developers + +add_subdirectory(generate_chainparams_constants) diff --git a/contrib/devtools/generate_chainparams_constants/CMakeLists.txt b/contrib/devtools/generate_chainparams_constants/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/contrib/devtools/generate_chainparams_constants/CMakeLists.txt @@ -0,0 +1,10 @@ +# Copyright (c) 2019 The Bitcoin developers + +add_custom_target(check-devtools + WORKING_DIRECTORY + ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND + ./test_generate_chainparams_constants.py +) + +add_dependencies(check-all check-devtools) diff --git a/contrib/devtools/generate_chainparams_constants/generate_chainparams_constants.py b/contrib/devtools/generate_chainparams_constants/generate_chainparams_constants.py new file mode 100755 --- /dev/null +++ b/contrib/devtools/generate_chainparams_constants/generate_chainparams_constants.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +from enum import Enum +import argparse +import os.path +import re +import sys + +sys.path.append('../../../test/functional/test_framework') +from authproxy import AuthServiceProxy + + +class Chain(Enum): + MainNet = "MAINNET" + TestNet = "TESTNET" + + +def get_chainparams(rpc_caller, block): + # Fetch initial chain info + chaininfo = rpc_caller.getblockchaininfo() + if chaininfo['chain'] == 'main': + chain = Chain.MainNet + else: + chain = Chain.TestNet + + # Use highest valid chainwork. This doesn't need to match the block hash + # used by assume valid. + chainwork = chaininfo['chainwork'] + if not re.match('^[0-9a-z]{64}$', chainwork): + raise Exception("Chain work is not a valid uint256 hex value.") + + # Default to N blocks from the chain tip, depending on which chain we're on + if not block: + block = chaininfo['blocks'] + if chain == Chain.MainNet: + block -= 10 + else: + block -= 2000 + + block = str(block) + if not re.match('^[0-9a-z]{64}$', block): + if re.match('^[0-9]*$', block): + # Fetch block hash using block height + block = rpc_caller.getblockhash(int(block)) + else: + raise Exception("Block hash is not a valid block hash or height.") + + # Make sure the block hash is part of the chain. This call with raise an + # exception if not. + rpc_caller.getblockheader(block) + + return (chain, chainwork, block) + + +def main(args): + input_file = args['input'] + output_file = args['output'] + + # If output_file exists and input_file isn't set, use output_file as input by default + if input_file is None and output_file is not None and os.path.isfile(output_file): + input_file = output_file + + (chain, chainwork, blockhash) = get_chainparams(args['rpc'], args['block']) + + # Get existing chainparams + params = [] + if input_file: + with open(input_file) as f: + for line in f: + if "_MINIMUM_CHAIN_WORK" in line or "_DEFAULT_ASSUME_VALID" in line: + # Exclude chainparams that match the chain being updated + if chain.value not in line: + params.append(line) + + params.append(" static const uint256 {}_DEFAULT_ASSUME_VALID = uint256S(\"{}\");\n".format( + chain.value, blockhash)) + params.append(" static const uint256 {}_MINIMUM_CHAIN_WORK = uint256S(\"{}\");\n".format( + chain.value, chainwork)) + params.sort() + + output = "" + output += "#ifndef BITCOIN_CHAINPARAMSCONSTANTS_H\n" + output += "#define BITCOIN_CHAINPARAMSCONSTANTS_H\n" + output += "/**\n" + output += " * Chain params constants for each tracked chain.\n" + output += " * @""generated by contrib/devtools/generate_chainparams_constants.py\n" + output += " */\n" + output += "\n" + output += "#include \n" + output += "\n" + output += "namespace ChainParamsConstants {\n" + for p in params: + output += p + output += "}\n" + output += "\n" + output += "#endif // BITCOIN_CHAINPARAMSCONSTANTS_H\n" + + if output_file: + with open(output_file, 'w') as f: + f.write(output) + return "" + + return output + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=( + "Generate chainparams constants header file." + "Prerequisites: RPC access to a bitcoind node.\n\n"), + formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('--address', '-a', default="127.0.0.1:8332", + help="Node address for making RPC calls.\n" + "The chain (MainNet or TestNet) will be automatically detected.\n" + "Default: '127.0.0.1:8332'") + parser.add_argument('--block', '-b', + help="The block hash or height to use for fetching chainparams.\n" + "MainNet default: 10 blocks from the chain tip." + "TestNet default: 2000 blocks from the chain tip.") + parser.add_argument('--config', '-c', default="~/.bitcoin/bitcoin.conf", + help="Path to bitcoin.conf for RPC authentication arguments (rpcuser & rpcpassword).\n" + "Default: ~/.bitcoin/bitcoin.conf") + parser.add_argument('--input', '-i', + help="Input file. Used to retain other constants not updated by this script.\n" + "Default: If the output is a file, it is used as initial input. Otherwise, none.") + parser.add_argument('--output', '-o', + help="Output file. If the file already exist, only changed values are replaced.\n" + "Default: Output goes to stdout.") + args = parser.parse_args() + args.config = os.path.expanduser(args.config) + + # Get user and password from config + user = None + password = None + if os.path.isfile(args.config): + with open(args.config, 'r', encoding='utf8') as f: + for line in f: + if line.startswith("rpcuser="): + # Ensure that there is only one rpcuser line + assert user is None + user = line.split("=")[1].strip("\n") + if line.startswith("rpcpassword="): + # Ensure that there is only one rpcpassword line + assert password is None + password = line.split("=")[1].strip("\n") + else: + raise FileNotFoundError("Missing bitcoin.conf") + if user is None: + raise ValueError("Config is missing rpcuser") + if password is None: + raise ValueError("Config is missing rpcpassword") + + args.rpc = AuthServiceProxy( + 'http://{}:{}@{}'.format(user, password, args.address)) + output = main(vars(args)) + if output: + print(output) diff --git a/contrib/devtools/generate_chainparams_constants/test_generate_chainparams_constants.py b/contrib/devtools/generate_chainparams_constants/test_generate_chainparams_constants.py new file mode 100755 --- /dev/null +++ b/contrib/devtools/generate_chainparams_constants/test_generate_chainparams_constants.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# Copyright (c) 2019 The Bitcoin developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import os +import re +import unittest + +from generate_chainparams_constants import main as GenerateChainParams + + +class MockRPC: + def __init__(self, test, chain, numBlocks, expectedBlock, blockHash, chainWork): + self.test = test + self.chain = chain + self.numBlocks = numBlocks + self.expectedBlock = expectedBlock + self.blockHash = blockHash + self.chainWork = chainWork + + def getblockchaininfo(self): + return { + "chain": self.chain, + "blocks": self.numBlocks, + "headers": self.numBlocks, + "bestblockhash": "0000000000000000039c96605d7fca74a5e185ea5634198346e9e07fd235b666", + "difficulty": 274412074285.6605, + "mediantime": 1562168718, + "verificationprogress": 0.9999958005632363, + "initialblockdownload": False, + "chainwork": self.chainWork, + "size_on_disk": 952031444, + "pruned": True, + "pruneheight": 582974, + "automatic_pruning": True, + "prune_target_size": 1048576000, + } + + def getblockhash(self, block): + # Tests should always request the right block height. Even though a + # real node will rarely raise an exception for this call, we are + # more strict during testing. + self.test.assertEqual(block, self.expectedBlock, "Called 'getblockhash {}' when expected was 'getblockhash {}'".format( + block, self.expectedBlock)) + return self.blockHash + + def getblockheader(self, blockHash): + # Make sure to raise an exception in the same way a real node would + # when calling 'getblockheader' on a block hash that is not part of + # the chain. + self.test.assertEqual(blockHash, self.blockHash, "Called 'getblockheader {}' when expected was 'getblockheader {}'".format( + blockHash, self.blockHash)) + return { + "hash": blockHash, + "confirmations": 1, + "height": 591463, + "version": 536870912, + "versionHex": "20000000", + "merkleroot": "51c898f034b6c5a5513a7c35912e86d009188311e550bb3096e04afb11f40aba", + "time": 1563212034, + "mediantime": 1563208994, + "nonce": 3501699724, + "bits": "18040cd6", + "difficulty": 271470800310.0635, + "chainwork": "000000000000000000000000000000000000000000f4c5e639fa012518a48a57", + "previousblockhash": "00000000000000000307b45e4a6cf8d49e70b9012ea1d72a5ce334a4213f66bd", + } + +# Provides a fail counter to fail after the Nth RPC command + + +class MockFailRPC(MockRPC): + def __init__(self, test, chain, numBlocks, expectedBlock, blockHash, chainWork, failCounter): + super().__init__(test, chain, numBlocks, expectedBlock, blockHash, chainWork) + self.failCounter = failCounter + + def checkFailCounter(self): + self.failCounter -= 1 + if self.failCounter < 0: + raise Exception("""error code: -99 + error message: + mock error""") + + def getblockchaininfo(self): + self.checkFailCounter() + return super().getblockchaininfo() + + def getblockhash(self, block): + self.checkFailCounter() + return super().getblockhash(block) + + def getblockheader(self, blockHash): + self.checkFailCounter() + return super().getblockheader(blockHash) + + +def CheckResult(test, args, expectedRegexes=None): + output = GenerateChainParams(args) + if args['output']: + with open(args['output']) as f: + output = f.read() + + for expectedRegex in expectedRegexes: + expectedPattern = re.compile(expectedRegex) + test.assertTrue(expectedPattern.search(output, re.MULTILINE), + "Expected pattern '{}' was not found in output:\n{}".format(expectedPattern, output)) + + +def CheckMockFailure(test, args, errorMessage='error code: -99'): + with test.assertRaises(Exception) as context: + GenerateChainParams(args) + test.assertIn(errorMessage, str(context.exception)) + + +class GenerateChainParamsTests(unittest.TestCase): + maxDiff = None + + def setUp(self): + self.testOutputFile = './testoutput' + self.blockHash1 = '0000000000000000003ef673ae12bc6017481830d37b9c52ce1e79c080e812b8' + self.chainWork1 = '000000000000000000000000000000000000000000f2537ccf2e07bbe15e70e1' + self.blockHash2 = '0000000000000298a9fa227f0ec32f2b7585f3e64c8b3369e7f8b4fd8ea3d836' + self.chainWork2 = '00000000000000000000000000000000000000000000004fdb4795a837f19671' + + def tearDown(self): + if os.path.isfile(self.testOutputFile): + os.remove(self.testOutputFile) + + def test_happy_path_mainnet(self): + mockRPC = MockRPC(test=self, chain='main', numBlocks=123000, + expectedBlock=122990, blockHash=self.blockHash1, chainWork=self.chainWork1) + args = { + 'rpc': mockRPC, + 'input': None, + 'output': None, + 'block': None, + } + CheckResult(self, args, [ + 'MAINNET_DEFAULT_ASSUME_VALID.*"0000000000000000003ef673ae12bc6017481830d37b9c52ce1e79c080e812b8"', + 'MAINNET_MINIMUM_CHAIN_WORK.*"000000000000000000000000000000000000000000f2537ccf2e07bbe15e70e1"', + ]) + + def test_happy_path_testnet(self): + mockRPC = MockRPC(test=self, chain='test', numBlocks=234000, + expectedBlock=232000, blockHash=self.blockHash1, chainWork=self.chainWork1) + args = { + 'rpc': mockRPC, + 'input': None, + 'output': None, + 'block': None, + } + CheckResult(self, args, [ + 'TESTNET_DEFAULT_ASSUME_VALID.*"0000000000000000003ef673ae12bc6017481830d37b9c52ce1e79c080e812b8"', + 'TESTNET_MINIMUM_CHAIN_WORK.*"000000000000000000000000000000000000000000f2537ccf2e07bbe15e70e1"', + ]) + + def test_wrong_chain(self): + mockRPC = MockRPC(test=self, chain='main', numBlocks=123000, + expectedBlock=122990, blockHash=self.blockHash1, chainWork=self.chainWork1) + args = { + 'rpc': mockRPC, + 'input': None, + 'output': None, + 'block': self.blockHash2, + } + CheckMockFailure( + self, args, "expected was 'getblockheader {}'".format(self.blockHash1)) + + def test_output_file(self): + # Generate first iteration of the output file + mockMainnetRPC = MockRPC(test=self, chain='main', numBlocks=123000, + expectedBlock=122990, blockHash=self.blockHash1, chainWork=self.chainWork1) + argsMainnet = { + 'rpc': mockMainnetRPC, + 'input': None, + 'output': self.testOutputFile, + 'block': None, + } + CheckResult(self, argsMainnet, [ + 'MAINNET_DEFAULT_ASSUME_VALID.*"0000000000000000003ef673ae12bc6017481830d37b9c52ce1e79c080e812b8"', + 'MAINNET_MINIMUM_CHAIN_WORK.*"000000000000000000000000000000000000000000f2537ccf2e07bbe15e70e1"', + ]) + + # Add testnet constants to the output file. Mainnet values should be retained. + mockTestnetRPC = MockRPC(test=self, chain='test', numBlocks=234000, + expectedBlock=232000, blockHash=self.blockHash2, chainWork=self.chainWork2) + argsTestnet = { + 'rpc': mockTestnetRPC, + 'input': self.testOutputFile, + 'output': self.testOutputFile, + 'block': None, + } + CheckResult(self, argsTestnet, [ + 'MAINNET_DEFAULT_ASSUME_VALID.*"0000000000000000003ef673ae12bc6017481830d37b9c52ce1e79c080e812b8"', + 'MAINNET_MINIMUM_CHAIN_WORK.*"000000000000000000000000000000000000000000f2537ccf2e07bbe15e70e1"', + 'TESTNET_DEFAULT_ASSUME_VALID.*"0000000000000298a9fa227f0ec32f2b7585f3e64c8b3369e7f8b4fd8ea3d836"', + 'TESTNET_MINIMUM_CHAIN_WORK.*"00000000000000000000000000000000000000000000004fdb4795a837f19671"', + ]) + + def test_bitcoin_cli_failures_testnet(self): + for chain in ['main', 'test']: + expectedBlock = 133990 + if chain == 'test': + expectedBlock = 132000 + + for failCounter in range(3): + mockFailRPC = MockFailRPC(test=self, chain=chain, numBlocks=134000, expectedBlock=expectedBlock, + blockHash=self.blockHash1, chainWork=self.chainWork1, failCounter=failCounter) + argsFail = { + 'rpc': mockFailRPC, + 'input': None, + 'output': None, + 'block': None, + } + CheckMockFailure(self, argsFail) + + +unittest.main() diff --git a/src/Makefile.am b/src/Makefile.am --- a/src/Makefile.am +++ b/src/Makefile.am @@ -117,6 +117,7 @@ chain.h \ chainparams.h \ chainparamsbase.h \ + chainparamsconstants.h \ chainparamsseeds.h \ checkpoints.h \ checkqueue.h \ diff --git a/src/chainparams.cpp b/src/chainparams.cpp --- a/src/chainparams.cpp +++ b/src/chainparams.cpp @@ -5,6 +5,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include @@ -104,13 +105,13 @@ consensus.fPowNoRetargeting = false; // The best chain should have at least this much work. - consensus.nMinimumChainWork = uint256S( - "000000000000000000000000000000000000000000f436d6f0fe3133f4c823c8"); + consensus.nMinimumChainWork = + ChainParamsConstants::MAINNET_MINIMUM_CHAIN_WORK; // By default assume that the signatures in ancestors of this block are // valid. - consensus.defaultAssumeValid = uint256S( - "000000000000000004425aeeb553758650da20031643fbfff0f53ac5dd1b39c3"); + consensus.defaultAssumeValid = + ChainParamsConstants::MAINNET_DEFAULT_ASSUME_VALID; // August 1, 2017 hard fork consensus.uahfHeight = 478558; @@ -267,13 +268,13 @@ consensus.fPowNoRetargeting = false; // The best chain should have at least this much work. - consensus.nMinimumChainWork = uint256S( - "00000000000000000000000000000000000000000000004fea236d62ac9a9f50"); + consensus.nMinimumChainWork = + ChainParamsConstants::TESTNET_MINIMUM_CHAIN_WORK; // By default assume that the signatures in ancestors of this block are // valid. - consensus.defaultAssumeValid = uint256S( - "000000000000037b3419370d2306ada6b4f30ac9f01a1bec513eef69b8cd4eab"); + consensus.defaultAssumeValid = + ChainParamsConstants::TESTNET_DEFAULT_ASSUME_VALID; // August 1, 2017 hard fork consensus.uahfHeight = 1155875; diff --git a/src/chainparamsconstants.h b/src/chainparamsconstants.h new file mode 100644 --- /dev/null +++ b/src/chainparamsconstants.h @@ -0,0 +1,17 @@ +#ifndef BITCOIN_CHAINPARAMSCONSTANTS_H +#define BITCOIN_CHAINPARAMSCONSTANTS_H +/** + * Chain params constants for each tracked chain. + * @generated by contrib/devtools/generate_chainparams_constants.py + */ + +#include + +namespace ChainParamsConstants { + static const uint256 MAINNET_DEFAULT_ASSUME_VALID = uint256S("000000000000000004425aeeb553758650da20031643fbfff0f53ac5dd1b39c3"); + static const uint256 MAINNET_MINIMUM_CHAIN_WORK = uint256S("000000000000000000000000000000000000000000f4c7abb607c9e277a6a267"); + static const uint256 TESTNET_DEFAULT_ASSUME_VALID = uint256S("000000000000037b3419370d2306ada6b4f30ac9f01a1bec513eef69b8cd4eab"); + static const uint256 TESTNET_MINIMUM_CHAIN_WORK = uint256S("000000000000000000000000000000000000000000000050db4824f6fe44c2da"); +} + +#endif // BITCOIN_CHAINPARAMSCONSTANTS_H