diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,3 +31,4 @@ add_subdirectory(src) add_subdirectory(test) +add_subdirectory(contrib/devtools/test) 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 + ./update-chainparams-test.py +) + +add_dependencies(check-all check-devtools) diff --git a/contrib/devtools/test/update-chainparams-test.py b/contrib/devtools/test/update-chainparams-test.py new file mode 100755 --- /dev/null +++ b/contrib/devtools/test/update-chainparams-test.py @@ -0,0 +1,211 @@ +#!/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 +from http.server import HTTPServer, BaseHTTPRequestHandler +import subprocess +import threading +import unittest + + +class Chain(Enum): + main = 1 + test = 2 + + +class MockRequestHandler(BaseHTTPRequestHandler): + chain = None + + def do_GET(self): + if self.path == '/rest/chaininfo.json': + self.wfile.write(str('{"chain":"' + self.chain.name + + '","bestblockhash":"0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44"' + ',"chainwork":"00000000000000000000000000000000000000000000004b3c801b9507361f33"}').encode()) + elif self.path == '/rest/block/0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44.json': + # Valid block hash + self.wfile.write( + '{"chainwork":"00000000000000000000000000000000000000000000004b3c801b9507361f33"}'.encode()) + elif self.path == '/rest/block/0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c45.json': + # Valid hash for non-existent block + self.wfile.write( + "0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c45 not found".encode()) + elif self.path == '/rest/block/abc.json': + # Invalid hash + self.wfile.write("Invalid hash: abc".encode()) + else: + self.server.failTest = "Unexpected request sent to mock server: {}".format( + self.path) + self.send_response(404) + return + + self.send_response(200) + + def log_request(self, code): + # Do not log requests otherwise it messes with the test framework output + pass + + +class MainnetMockRequestHandler(MockRequestHandler): + chain = Chain.main + + +class TestnetMockRequestHandler(MockRequestHandler): + chain = Chain.test + + +def CheckResult(test, args, expectedExitCode, expectedPatch=None): + # Sanity check that chainparams doesn't have uncommitted changes prior to running the script + test.assertEqual(1, subprocess.run('git diff-index HEAD | grep "src/chainparams.cpp"', shell=True, stdout=subprocess.PIPE, + stderr=subprocess.PIPE).returncode, "src/chainparams.cpp cannot have uncommitted changes prior to running this test!") + + try: + test.assertEqual(expectedExitCode, + subprocess.run('../update-chainparams.py {}'.format(args), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode, "Incorrect exit code. Expected exit code was {}.".format(expectedExitCode)) + except: + raise + else: + # This produces a condensed patch file that removes the line numbers + # without breaking context. This allows us to test that the patch + # contents are correct without breaking these tests whenever code is + # changed elsewhere in the file. + p = subprocess.Popen( + 'git diff -- :/src/chainparams.cpp | grep -A 1 "[-] "', shell=True, stdout=subprocess.PIPE) + output = p.communicate()[0] + output = output.decode('utf-8') + if expectedPatch is None: + test.assertEqual('', output) + else: + test.assertEqual(expectedPatch, output) + finally: + # Cleanup changes to chainparams + subprocess.run('git checkout HEAD -- :/src/chainparams.cpp', + shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + +class MockHTTPServer(HTTPServer): + timeout = 1 + failTest = None + + def __init__(self, addr, handler, test): + self.test = test + super(HTTPServer, self).__init__(addr, handler) + + def handle_timeout(self): + self.test.fail("Request timed out.") + + +class MainnetTests(unittest.TestCase): + maxDiff = None + + def setUp(self): + self.expectedPatch = ('--- a/src/chainparams.cpp\n' + '+++ b/src/chainparams.cpp\n' + '--\n' + '- "000000000000000000000000000000000000000000eff3c3a458ba78bf184946");\n' + '+ "00000000000000000000000000000000000000000000004b3c801b9507361f33");\n' + '--\n' + '- "0000000000000000023b7cce03f549ba13097b7f9dba787e8b7e7e9f231e0463");\n' + '+ "0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44");\n') + self.server = MockHTTPServer( + ('', 8332), MainnetMockRequestHandler, self) + threading.Thread(target=self.server.serve_forever, daemon=True).start() + + def tearDown(self): + self.server.shutdown() + self.server.server_close() + if self.server.failTest is not None: + self.fail(self.server.failTest) + + def test_help(self): + CheckResult(self, '-h', 0) + CheckResult(self, '-t -h', 0) + + def test_mainnet_args_on_mainnet(self): + CheckResult(self, '', 0, self.expectedPatch) + CheckResult(self, '-a localhost', 0, self.expectedPatch) + CheckResult(self, '-a notlocalhost', 1) + CheckResult(self, '-p 9999', 1) + CheckResult(self, '-p 8332', 0, self.expectedPatch) + CheckResult(self, '-a localhost -p 8332', 0, self.expectedPatch) + CheckResult(self, '-a notlocalhost -p 8332', 1) + + CheckResult(self, '-b abc', 2) + CheckResult( + self, '-b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c45', 2) + CheckResult( + self, '-b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44', 0, self.expectedPatch) + + def test_testnet_args_on_mainnet(self): + CheckResult(self, '-t', 1) + CheckResult(self, '-t -a localhost', 1) + CheckResult(self, '-t -a notlocalhost', 1) + CheckResult(self, '-t -p 9999', 1) + CheckResult(self, '-t -p 8332', 3) + CheckResult(self, '-t -a localhost -p 8332', 3) + CheckResult(self, '-t -a notlocalhost -p 8332', 1) + + CheckResult(self, '-t -p 8332 -b abc', 3) + CheckResult( + self, '-t -p 8332 -b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c45', 3) + CheckResult( + self, '-t -p 8332 -b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44', 3) + + +class TestnetTests(unittest.TestCase): + maxDiff = None + + def setUp(self): + self.expectedPatch = ('--- a/src/chainparams.cpp\n' + '+++ b/src/chainparams.cpp\n' + '--\n' + '- "00000000000000000000000000000000000000000000004ec11c53d25a577f28");\n' + '+ "00000000000000000000000000000000000000000000004b3c801b9507361f33");\n' + '--\n' + '- "000000000000001b564c7e20d8f002203a6b7029f440edece8c7b2e549b8202f");\n' + '+ "0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44");\n') + self.server = MockHTTPServer( + ('', 18332), TestnetMockRequestHandler, self) + threading.Thread(target=self.server.serve_forever, daemon=True).start() + + def tearDown(self): + self.server.shutdown() + self.server.server_close() + + def test_help(self): + CheckResult(self, '-h', 0) + CheckResult(self, '-t -h', 0) + + def test_mainnet_args_on_testnet(self): + CheckResult(self, '', 1) + CheckResult(self, '-a localhost', 1) + CheckResult(self, '-a notlocalhost', 1) + CheckResult(self, '-p 99999', 1) + CheckResult(self, '-p 18332', 3) + CheckResult(self, '-a localhost -p 18332', 3) + CheckResult(self, '-a notlocalhost -p 18332', 1) + + CheckResult(self, '-p 18332 -b abc', 3) + CheckResult( + self, '-p 18332 -b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c45', 3) + CheckResult( + self, '-p 18332 -b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44', 3) + + def test_testnet_args_on_mainnet(self): + CheckResult(self, '-t', 0, self.expectedPatch) + CheckResult(self, '-t -a localhost', 0, self.expectedPatch) + CheckResult(self, '-t -a notlocalhost', 1) + CheckResult(self, '-t -p 99999', 1) + CheckResult(self, '-t -p 18332', 0, self.expectedPatch) + CheckResult(self, '-t -a localhost -p 18332', 0, self.expectedPatch) + CheckResult(self, '-t -a notlocalhost -p 18332', 1) + + CheckResult(self, '-t -b abc', 2) + CheckResult( + self, '-t -b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c45', 2) + CheckResult( + self, '-t -b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44', 0, self.expectedPatch) + + +unittest.main() diff --git a/contrib/devtools/update-chainparams.py b/contrib/devtools/update-chainparams.py new file mode 100755 --- /dev/null +++ b/contrib/devtools/update-chainparams.py @@ -0,0 +1,93 @@ +#!/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 argparse +import json +import re +import subprocess +import sys + +parser = argparse.ArgumentParser(description=( + "Fetch a block from the bitcoin HTTP RPC to update chainparams" + "(nMinimumChainWork and defaultAssumeValid).\n\n" + "NOTE: bitcoind must have `--rest=1` set (either as a cli argument or in" + "the config)!"), formatter_class=argparse.RawTextHelpFormatter) +parser.add_argument('--hostaddr', '-a', + default="localhost", + help="Host address of the HTTP RPC bitcoind server to call" + "for fetching chain info. (defaults to `localhost`)") +parser.add_argument('--hostport', '-p', + help="Host port of the HTTP RPC bitcoind server to call" + "for fetching chain info. (defaults to 8332 on mainnet and 18332 on testnet)") +parser.add_argument('--testnet', '-t', action="store_true", + help="If set, testnet chainparams will be updated instead" + "of mainnet chainparams. If testnet is set, but hostport is not set, hostport will default to 18332.") +parser.add_argument('--blockhash', '-b', + help="(optional) The block hash to update chainparams" + "with. If not specified, bestblockhash is used.") +args = parser.parse_args() + + +def curl(url): + # Call bitcoin HTTP RPC using curl + print("Fetching URL: {}".format(url)) + curlArgs = ['curl', '-s', url] + p = subprocess.Popen('curl -s ' + url, stdout=subprocess.PIPE, shell=True) + output = p.communicate()[0] + status = p.wait() + if status != 0: + print("curl error status: {}\nSee https://curl.haxx.se/libcurl/c/libcurl-errors.html for error descriptions.".format(status)) + exit(1) + output = output.decode('utf-8') + try: + return json.loads(output) + except: + print("Error while parsing JSON:\n{}".format(sys.exc_info())) + print("Output from HTTP RPC call:\n{}".format(output)) + exit(2) + + +port = args.hostport +if not port: + port = '18332' if args.testnet else '8332' +url = args.hostaddr + ':' + port + +# Get best block hash and chainwork +chaininfo = curl(url + '/rest/chaininfo.json') + +if (args.testnet is True and chaininfo['chain'] != 'test') or (args.testnet is False and chaininfo['chain'] != 'main'): + print("The chain network specified did not match the one given by the host node!") + print("Did you mean to call with a different `--hostaddr` or `--hostport`, or with `--testnet` set?") + exit(3) + +blockhash = chaininfo['bestblockhash'] +chainwork = chaininfo['chainwork'] +if args.blockhash: + blockhash = args.blockhash + print("Fetching chainwork for block {}".format(blockhash)) + block = curl(url + '/rest/block/' + blockhash + '.json') + chainwork = block['chainwork'] + +chainParamsClass = 'TestNetParams' if args.testnet else 'MainParams' + +# Get top-level path for the project +p = subprocess.Popen('git rev-parse --show-toplevel', + stdout=subprocess.PIPE, shell=True) +toplevel = p.communicate()[0] +p.wait() + +# Read-write to chainparams.cpp +f = open(toplevel.decode('utf-8').rstrip() + '/src/chainparams.cpp', 'r+') +chainParamsContents = f.read() + +# Replace chainparams with the new values for the specified chainparams class +chainParamsContents = re.sub('(' + chainParamsClass + '.*?nMinimumChainWork.*?")([0-9a-z]+)("\);)', + '\g<1>' + chainwork + '\g<3>', chainParamsContents, flags=re.MULTILINE | re.DOTALL) +chainParamsContents = re.sub('(' + chainParamsClass + '.*?defaultAssumeValid.*?")([0-9a-z]+)("\);)', + '\g<1>' + blockhash + '\g<3>', chainParamsContents, flags=re.MULTILINE | re.DOTALL) + +f.seek(0) +f.write(chainParamsContents) +f.close()