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/test) add_subdirectory(doc) diff --git a/contrib/devtools/generate_bestchainparams.py b/contrib/devtools/generate_bestchainparams.py new file mode 100755 --- /dev/null +++ b/contrib/devtools/generate_bestchainparams.py @@ -0,0 +1,138 @@ +#!/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 enum +import json +import os +import re +import stat +import subprocess +import sys + + +class Chain(Enum): + MainNet = "MainNet" + TestNet = "TestNet" + + +def call_rpc_shell(cli, args): + shell_call = '{} {}'.format(cli, args) + p = subprocess.Popen(shell_call, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + stdout, stderr = p.communicate() + exit_code = p.wait() + return shell_call, exit_code, stdout.decode('utf-8'), stderr.decode('utf-8') + + +def exit_code_check(shell_call, exit_code, stdout, stderr): + if exit_code != 0: + raise Exception("Error calling '{}'\nExit code: {}\nstdout:\n{}\nstderr:\n{}".format(shell_call, exit_code, stdout, stderr)) + return stdout + + +def make_bitcoincli_caller(cli): + return lambda args: call_rpc_shell(cli, args) + + +def parse_json(s): + try: + return json.loads(s) + except: + raise Exception("Error while parsing JSON:\n{}\nInput string:\n{}".format(sys.exc_info(), s)) + + +def get_best_chainparams(rpc_caller, chain, block): + # Fetch initial chain info + chaininfo = parse_json(rpc_caller('getblockchaininfo')) + + # Make sure the node is on the expected chain + currentChain = chaininfo['chain'] + if (chain == Chain.MainNet and currentChain != "main") or (chain == Chain.TestNet and currentChain != "test"): + raise Exception("The chain network specified did not match the one given by the host node!\nYou requested: {}\nHost node is serving: {}".format(chain, currentChain)) + + # 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 -= 2 + 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 {}'.format(block)).rstrip() + else: + raise Exception("Block hash is not a valid block hash or height.") + + # Fetch block chain work + blockheader = parse_json(rpc_caller('getblockheader {}'.format(block))) + chainwork = blockheader['chainwork'] + if not re.match('^[0-9a-z]{64}$', chainwork): + raise Exception("Chain work is not a valid uint256 hex value.") + + return (chainwork, block) + + +def main(args, make_rpc_caller): + parser = argparse.ArgumentParser(description=( + "Generate a chainparams best chain header file." + "Prerequisites: Two bitcoind nodes running, mainnet and testnet.\n\n"), + formatter_class=argparse.RawTextHelpFormatter) + parser.add_argument('--cli', '-c', default="./bitcoin-cli", + help="The path to the bitcoin-cli binary.\nDefault: ./bitcoin-cli") + parser.add_argument('--mainnetblock', '-m', + help="The block hash or height to use for fetching MainNet chainparams.\n" + "Default: 2 blocks from the chain tip.") + parser.add_argument('--testnetblock', '-t', + help="The block hash or height to use for fetching TestNet chainparams.\n" + "Default: 2000 blocks from the chain tip.") + args = parser.parse_args(args) + + chains = {} + chains[Chain.MainNet] = args.mainnetblock + chains[Chain.TestNet] = args.testnetblock + + params = {} + for chain, block in chains.items(): + cli = args.cli + if not cli: + cli = './bitcoin-cli' + if chain == Chain.TestNet: + cli += " --testnet" + + # Wrap rpc caller with error checker + rpc_caller = lambda args: exit_code_check(*(make_rpc_caller(cli))(args)) + + params[chain] = get_best_chainparams(rpc_caller, chain, block) + + output = "" + output += "// Copyright (c) 2019 The Bitcoin developers\n" + output += "// Distributed under the MIT software license, see the accompanying\n" + output += "// file COPYING or http://www.opensource.org/licenses/mit-license.php.\n" + output += "\n" + output += "#ifndef BITCOIN_CHAINPARAMSBESTCHAIN_H\n" + output += "#define BITCOIN_CHAINPARAMSBESTCHAIN_H\n" + output += "/**\n" + output += " * Best block hash and chain work chain params for each tracked chain.\n" + output += " * @generated by contrib/devtools/generate-bestchainparams.sh\n" + output += " */\n" + output += "\n" + output += "#include \n" + output += "\n" + output += "namespace ChainParamsBestChain {\n" + for chain, (chainwork, blockhash) in params.items(): + output += " static const uint256 {}MinimumChainWork = uint256S(\"{}\");\n".format(chain.name, chainwork) + output += " static const uint256 {}DefaultAssumeValid = uint256S(\"{}\");\n".format(chain.name, blockhash) + output += "}\n" + output += "\n" + output += "#endif // BITCOIN_CHAINPARAMSBESTCHAIN_H" + return output + + +if __name__ == "__main__": + print(main(sys.argv[1:], make_bitcoincli_caller)) diff --git a/contrib/devtools/test/CMakeLists.txt b/contrib/devtools/test/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/contrib/devtools/test/CMakeLists.txt @@ -0,0 +1,9 @@ + +add_custom_target(check-devtools + WORKING_DIRECTORY + ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND + ./generate_bestchainparams_test.py +) + +add_dependencies(check-all check-devtools) diff --git a/contrib/devtools/test/generate_bestchainparams_test.py b/contrib/devtools/test/generate_bestchainparams_test.py new file mode 100755 --- /dev/null +++ b/contrib/devtools/test/generate_bestchainparams_test.py @@ -0,0 +1,197 @@ +#!/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 sys +sys.path.append('..') +from generate_bestchainparams import main as GenerateBestChainParams + +import re +import unittest + + +def MockRPCResult(shell_call, exit_code, stdout): + # This method's output is used by generate_bestchainparams.py internally, + # so the resulting tuple should always match the form: + # shell_call (command + args), exit_code, stdout, stderr + return shell_call, exit_code, stdout, '' + + +def GetShellString(args, testnet): + bitcoin_cli = './bitcoin-cli' + if testnet: + bitcoin_cli += ' --testnet' + return '{} {}'.format(bitcoin_cli, args) + + +def rpcMock_getblockchaininfo(args, chain, numBlocks, testnet=False): + return MockRPCResult(GetShellString(args, testnet), 0, """{{ + "chain": "{chain}", + "blocks": {numBlocks}, + "headers": {numBlocks}, + "bestblockhash": "0000000000000000039c96605d7fca74a5e185ea5634198346e9e07fd235b666", + "difficulty": 274412074285.6605, + "mediantime": 1562168718, + "verificationprogress": 0.9999958005632363, + "initialblockdownload": false, + "chainwork": "000000000000000000000000000000000000000000f3145d6494c45afcc9e357", + "size_on_disk": 952031444, + "pruned": true, + "pruneheight": 582974, + "automatic_pruning": true, + "prune_target_size": 1048576000 + }}""".format(chain=chain, numBlocks=numBlocks)) + + +def rpcMock_getblockhash(args, blockhash, testnet=False): + return MockRPCResult(GetShellString(args, testnet), 0, blockhash) + + +def rpcMock_getblockheader(args, blockhash, chainwork, testnet=False): + return MockRPCResult(GetShellString(args, testnet), 0, """{{ + "hash": "{blockhash}", + "confirmations": 12732, + "height": 1300000, + "version": 536870912, + "versionHex": "20000000", + "merkleroot": "78fe397a5163796e7df159df5aa92096c92ec3cbf43955b996bfc0d347853e86", + "time": 1556004882, + "mediantime": 1555998692, + "nonce": 2401154079, + "bits": "1d00ffff", + "difficulty": 1, + "chainwork": "{chainwork}", + "previousblockhash": "00000000000303bb4d23987f5432c5e9ec6d05caed576c102f9e0f74e1d8fc6e", + "nextblockhash": "000000001ab7a68eb3e41dad9f4515344a3865da19363a54a691f7524819be9f" + }}""".format(blockhash=blockhash, chainwork=chainwork)) + + +def RPCMatcher(requests_and_responses, shell_call): + if shell_call in requests_and_responses: + return requests_and_responses[shell_call] + else: + return MockRPCResult(shell_call, 99, """error code: -99 + error message: + mock error""") + + +def RPCFactory(requests_and_responses): + return lambda cli: lambda request_args: RPCMatcher(requests_and_responses, '{} {}'.format(cli, request_args)) + + +def MakeRPCFactory(rpc_result_list): + rpc_map = {} + for rpc_result in rpc_result_list: + rpc_map[rpc_result[0]] = rpc_result + return RPCFactory(rpc_map) + + +def CheckResult(test, stdout, expectedRegexes=None): + for expectedRegex in expectedRegexes: + expectedPattern = re.compile(expectedRegex) + test.assertTrue(expectedPattern.search(stdout, re.MULTILINE), + "Expected pattern '{}' was not found.".format(expectedPattern)) + + +def CheckMockFailure(test, args, factoryMap): + with test.assertRaises(Exception) as context: + GenerateBestChainParams(args, factoryMap) + CheckResult(test, str(context.exception), ['error code: -99']) + + +class GenerateBestChainParamsTests(unittest.TestCase): + maxDiff = None + + def setUp(self): + defaultMainnetBlockHash = '0000000000000000003ef673ae12bc6017481830d37b9c52ce1e79c080e812b8' + defaultMainnetChainWork = '000000000000000000000000000000000000000000f2537ccf2e07bbe15e70e1' + self.defaultFactoriesMainnet = [ + rpcMock_getblockchaininfo('getblockchaininfo', 'main', 123000), + rpcMock_getblockhash('getblockhash 122998', + defaultMainnetBlockHash), + rpcMock_getblockheader('getblockheader {}'.format( + defaultMainnetBlockHash), defaultMainnetBlockHash, defaultMainnetChainWork), + ] + + defaultTestnetBlockHash = '0000000000000298a9fa227f0ec32f2b7585f3e64c8b3369e7f8b4fd8ea3d836' + defaultTestnetChainWork = '00000000000000000000000000000000000000000000004fdb4795a837f19671' + self.defaultFactoriesTestnet = [ + rpcMock_getblockchaininfo( + 'getblockchaininfo', 'test', 234000, testnet=True), + rpcMock_getblockhash('getblockhash 232000', + defaultTestnetBlockHash, testnet=True), + rpcMock_getblockheader('getblockheader {}'.format( + defaultTestnetBlockHash), defaultTestnetBlockHash, defaultTestnetChainWork, testnet=True), + ] + + altMainnetBlockHash = '000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201' + altMainnetChainWork = '0000000000000000000000000000000000000000007ae48aca46e3b449ad9714' + self.altFactoriesMainnet = [ + rpcMock_getblockchaininfo('getblockchaininfo', 'main', 345000), + rpcMock_getblockhash('getblockhash 345000', altMainnetBlockHash), + rpcMock_getblockheader('getblockheader {}'.format( + altMainnetBlockHash), altMainnetBlockHash, altMainnetChainWork), + ] + + altTestnetBlockHash = '000000002a7a59c4f88a049fa5e405e67cd689d75a1f330cbf26286cf0ec1d8f' + altTestnetChainWork = '00000000000000000000000000000000000000000000004802a671314f8f803f' + self.altFactoriesTestnet = [ + rpcMock_getblockchaininfo( + 'getblockchaininfo', 'test', 456000, testnet=True), + rpcMock_getblockhash('getblockhash 456000', + altTestnetBlockHash, testnet=True), + rpcMock_getblockheader('getblockheader {}'.format( + altTestnetBlockHash), altTestnetBlockHash, altTestnetChainWork, testnet=True), + ] + + def test_happy_path(self): + factoryMap = MakeRPCFactory( + self.defaultFactoriesMainnet + self.defaultFactoriesTestnet) + CheckResult(self, GenerateBestChainParams([], factoryMap), [ + 'MainNetMinimumChainWork.*"000000000000000000000000000000000000000000f2537ccf2e07bbe15e70e1"', + 'MainNetDefaultAssumeValid.*"0000000000000000003ef673ae12bc6017481830d37b9c52ce1e79c080e812b8"', + 'TestNetMinimumChainWork.*"00000000000000000000000000000000000000000000004fdb4795a837f19671"', + 'TestNetDefaultAssumeValid.*"0000000000000298a9fa227f0ec32f2b7585f3e64c8b3369e7f8b4fd8ea3d836"', + ]) + + def test_happy_path_explicit_mainnet(self): + factoryMap = MakeRPCFactory( + self.altFactoriesMainnet + self.defaultFactoriesTestnet) + CheckResult(self, GenerateBestChainParams(['-m', '345000'], factoryMap), [ + 'MainNetMinimumChainWork.*"0000000000000000000000000000000000000000007ae48aca46e3b449ad9714"', + 'MainNetDefaultAssumeValid.*"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201"', + 'TestNetMinimumChainWork.*"00000000000000000000000000000000000000000000004fdb4795a837f19671"', + 'TestNetDefaultAssumeValid.*"0000000000000298a9fa227f0ec32f2b7585f3e64c8b3369e7f8b4fd8ea3d836"', + ]) + + def test_happy_path_explicit_testnet(self): + factoryMap = MakeRPCFactory( + self.defaultFactoriesMainnet + self.altFactoriesTestnet) + CheckResult(self, GenerateBestChainParams(['-t', '456000'], factoryMap), [ + 'MainNetMinimumChainWork.*"000000000000000000000000000000000000000000f2537ccf2e07bbe15e70e1"', + 'MainNetDefaultAssumeValid.*"0000000000000000003ef673ae12bc6017481830d37b9c52ce1e79c080e812b8"', + 'TestNetMinimumChainWork.*"00000000000000000000000000000000000000000000004802a671314f8f803f"', + 'TestNetDefaultAssumeValid.*"000000002a7a59c4f88a049fa5e405e67cd689d75a1f330cbf26286cf0ec1d8f"', + ]) + + def test_happy_path_explicit_mainnet_and_testnet(self): + factoryMap = MakeRPCFactory( + self.altFactoriesMainnet + self.altFactoriesTestnet) + CheckResult(self, GenerateBestChainParams(['-m', '345000', '-t', '456000'], factoryMap), [ + 'MainNetMinimumChainWork.*"0000000000000000000000000000000000000000007ae48aca46e3b449ad9714"', + 'MainNetDefaultAssumeValid.*"000000000000000005e14d3f9fdfb70745308706615cfa9edca4f4558332b201"', + 'TestNetMinimumChainWork.*"00000000000000000000000000000000000000000000004802a671314f8f803f"', + 'TestNetDefaultAssumeValid.*"000000002a7a59c4f88a049fa5e405e67cd689d75a1f330cbf26286cf0ec1d8f"', + ]) + + def test_bitcoin_cli_failures(self): + for i in range(len(self.defaultFactoriesMainnet)): + CheckMockFailure(self, '', MakeRPCFactory( + self.defaultFactoriesTestnet + self.defaultFactoriesMainnet[:i])) + for i in range(len(self.defaultFactoriesTestnet)): + CheckMockFailure(self, '', MakeRPCFactory( + self.defaultFactoriesMainnet + self.defaultFactoriesTestnet[:i])) + + +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 \ + chainparamsbestchain.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( - "000000000000000000000000000000000000000000f22fbd89943b5f5104e4ec"); + consensus.nMinimumChainWork = + ChainParamsBestChain::MainNetMinimumChainWork; // By default assume that the signatures in ancestors of this block are // valid. - consensus.defaultAssumeValid = uint256S( - "00000000000000000401095ca2933cc4729484965e66e6a9f8e937070cc8e971"); + consensus.defaultAssumeValid = + ChainParamsBestChain::MainNetDefaultAssumeValid; // August 1, 2017 hard fork consensus.uahfHeight = 478558; @@ -269,13 +270,13 @@ consensus.fPowNoRetargeting = false; // The best chain should have at least this much work. - consensus.nMinimumChainWork = uint256S( - "00000000000000000000000000000000000000000000004f587a0e52b7984751"); + consensus.nMinimumChainWork = + ChainParamsBestChain::TestNetMinimumChainWork; // By default assume that the signatures in ancestors of this block are // valid. - consensus.defaultAssumeValid = uint256S( - "00000000001b618c015c41cc218a60a5a94bc42e16e30f1426cfc138615201c3"); + consensus.defaultAssumeValid = + ChainParamsBestChain::TestNetDefaultAssumeValid; // August 1, 2017 hard fork consensus.uahfHeight = 1155875; diff --git a/src/chainparamsbestchain.h b/src/chainparamsbestchain.h new file mode 100644 --- /dev/null +++ b/src/chainparamsbestchain.h @@ -0,0 +1,21 @@ +// 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. + +#ifndef BITCOIN_CHAINPARAMSBESTCHAIN_H +#define BITCOIN_CHAINPARAMSBESTCHAIN_H +/** + * Best block hash and chain work chain params for each tracked chain. + * @generated by contrib/devtools/generate-bestchainparams.sh + */ + +#include + +namespace ChainParamsBestChain { + static const uint256 MainNetMinimumChainWork = uint256S("000000000000000000000000000000000000000000f22fbd89943b5f5104e4ec"); + static const uint256 MainNetDefaultAssumeValid = uint256S("00000000000000000401095ca2933cc4729484965e66e6a9f8e937070cc8e971"); + static const uint256 TestNetMinimumChainWork = uint256S("00000000000000000000000000000000000000000000004f587a0e52b7984751"); + static const uint256 TestNetDefaultAssumeValid = uint256S("00000000001b618c015c41cc218a60a5a94bc42e16e30f1426cfc138615201c3"); +} + +#endif // BITCOIN_CHAINPARAMSBESTCHAIN_H