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/fetch-chainparams.py b/contrib/devtools/fetch-chainparams.py new file mode 100755 --- /dev/null +++ b/contrib/devtools/fetch-chainparams.py @@ -0,0 +1,72 @@ +#!/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 subprocess +import sys + +parser = argparse.ArgumentParser(description=( + "Fetch a block's chainparams values from the bitcoin HTTP RPC" + "(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), file=sys.stderr) + 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), file=sys.stderr) + exit(1) + output = output.decode('utf-8') + try: + return json.loads(output) + except: + print("Error while parsing JSON:\n{}".format(sys.exc_info()), file=sys.stderr) + print("Output from HTTP RPC call:\n{}".format(output), file=sys.stderr) + 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!", file=sys.stderr) + print("Did you mean to call with a different `--hostaddr` or `--hostport`, or with `--testnet` set?", file=sys.stderr) + exit(3) + +blockhash = chaininfo['bestblockhash'] +chainwork = chaininfo['chainwork'] +if args.blockhash: + blockhash = args.blockhash + print("Fetching chainwork for block {}".format(blockhash), file=sys.stderr) + block = curl(url + '/rest/block/' + blockhash + '.json') + chainwork = block['chainwork'] + +print(json.dumps({'chainwork': chainwork, 'blockhash': blockhash})) 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 + ./fetch-chainparams-test.py +) + +add_dependencies(check-all check-devtools) diff --git a/contrib/devtools/test/fetch-chainparams-test.py b/contrib/devtools/test/fetch-chainparams-test.py new file mode 100755 --- /dev/null +++ b/contrib/devtools/test/fetch-chainparams-test.py @@ -0,0 +1,194 @@ +#!/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 json +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, expectedOutput='', outputContains=None): + try: + r = subprocess.run('../fetch-chainparams.py {}'.format(args), + shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + test.assertEqual(expectedExitCode, r.returncode, "Incorrect exit code: {}\nExpected exit code was: {}\nStderr returned:\n{}".format( + r.returncode, expectedExitCode, r.stderr)) + + actualOutput = r.stdout.decode('utf-8').rstrip() + if outputContains is None: + if len(actualOutput) > 0: + actualOutput = json.loads(actualOutput) + test.assertEqual(expectedOutput, actualOutput, "Expected output didn't match!\nActual:\n{}\nExpected:\n{}".format( + actualOutput, expectedOutput)) + else: + test.assertIn(outputContains, actualOutput) + except: + raise + + +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.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, + outputContains="usage: fetch-chainparams.py") + CheckResult(self, '-t -h', 0, + outputContains="usage: fetch-chainparams.py") + + def test_mainnet_args_on_mainnet(self): + expectedOutput = {'chainwork': '00000000000000000000000000000000000000000000004b3c801b9507361f33', + 'blockhash': '0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44'} + CheckResult(self, '', 0, expectedOutput) + CheckResult(self, '-a localhost', 0, expectedOutput) + CheckResult(self, '-a notlocalhost', 1) + CheckResult(self, '-p 9999', 1) + CheckResult(self, '-p 8332', 0, expectedOutput) + CheckResult(self, '-a localhost -p 8332', 0, expectedOutput) + CheckResult(self, '-a notlocalhost -p 8332', 1) + + CheckResult(self, '-b abc', 2) + CheckResult( + self, '-b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c45', 2) + CheckResult( + self, '-b 0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44', 0, expectedOutput) + + 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.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, + outputContains="usage: fetch-chainparams.py") + CheckResult(self, '-t -h', 0, + outputContains="usage: fetch-chainparams.py") + + 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): + expectedOutput = {'chainwork': '00000000000000000000000000000000000000000000004b3c801b9507361f33', + 'blockhash': '0000000000073b088995d4dac03809c1c647c231523f69e2db99bbfb1c574c44'} + CheckResult(self, '-t', 0, expectedOutput) + CheckResult(self, '-t -a localhost', 0, expectedOutput) + CheckResult(self, '-t -a notlocalhost', 1) + CheckResult(self, '-t -p 99999', 1) + CheckResult(self, '-t -p 18332', 0, expectedOutput) + CheckResult(self, '-t -a localhost -p 18332', 0, expectedOutput) + 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, expectedOutput) + + +unittest.main()