diff --git a/.arclint b/.arclint index ad83a5633..3d9389d77 100644 --- a/.arclint +++ b/.arclint @@ -1,341 +1,341 @@ { "linters": { "generated": { "type": "generated" }, "clang-format": { "type": "clang-format", "version": ">=12.0", "bin": [ "clang-format-12", "clang-format" ], "include": "(^(src|chronik)/.*\\.(h|c|cpp|mm)$)", "exclude": [ "(^src/(secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)" ] }, "autopep8": { "type": "autopep8", "version": ">=1.3.4", "include": "(\\.py$)", "exclude": [ "(^contrib/gitian-builder/)", "(^contrib/apple-sdk-tools/)", - "(^test/functional/[a-fmt].*\\.py$)" + "(^test/functional/[a-mt].*\\.py$)" ], "flags": [ "--aggressive", "--ignore=W503,W504", "--max-line-length=88" ] }, "black": { "type": "black", "version": ">=23.0.0", "include": [ - "(^test/functional/[a-fmt].*\\.py$)" + "(^test/functional/[a-mt].*\\.py$)" ], "flags": [ "--preview" ] }, "flake8": { "type": "flake8", "version": ">=5.0", "include": "(\\.py$)", "exclude": [ "(^contrib/gitian-builder/)", "(^contrib/apple-sdk-tools/)" ], "flags": [ "--ignore=A003,E203,E303,E305,E501,E704,W503,W504", "--require-plugins=flake8-comprehensions,flake8-builtins" ] }, "lint-format-strings": { "type": "lint-format-strings", "include": "(^(src|chronik)/.*\\.(h|c|cpp)$)", "exclude": [ "(^src/(secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)", "(^src/test/fuzz/strprintf.cpp$)" ] }, "check-doc": { "type": "check-doc", "include": "(^(src|chronik)/.*\\.(h|c|cpp)$)" }, "lint-tests": { "type": "lint-tests", "include": "(^src/(seeder/|rpc/|wallet/)?test/.*\\.(cpp)$)" }, "phpcs": { "type": "phpcs", "include": "(\\.php$)", "exclude": [ "(^arcanist/__phutil_library_.+\\.php$)" ], "phpcs.standard": "arcanist/phpcs.xml" }, "lint-locale-dependence": { "type": "lint-locale-dependence", "include": "(^(src|chronik)/.*\\.(h|cpp)$)", "exclude": [ "(^src/(crypto/ctaes/|leveldb/|secp256k1/|tinyformat.h|univalue/))", "(^src/bench/nanobench.h$)" ] }, "lint-cheader": { "type": "lint-cheader", "include": "(^(src|chronik)/.*\\.(h|cpp)$)", "exclude": [ "(^src/(crypto/ctaes|secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)" ] }, "spelling": { "type": "spelling", "exclude": [ "(^build-aux/m4/)", "(^depends/)", "(^doc/release-notes/)", "(^contrib/gitian-builder/)", "(^src/(qt/locale|secp256k1|univalue|leveldb)/)", "(^test/lint/dictionary/)", "(package-lock.json)" ], "spelling.dictionaries": [ "test/lint/dictionary/english.json" ] }, "lint-assert-with-side-effects": { "type": "lint-assert-with-side-effects", "include": "(^(src|chronik)/.*\\.(h|cpp)$)", "exclude": [ "(^src/(secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)" ] }, "lint-include-quotes": { "type": "lint-include-quotes", "include": "(^(src|chronik)/.*\\.(h|cpp)$)", "exclude": [ "(^src/(secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)" ] }, "lint-include-guard": { "type": "lint-include-guard", "include": "(^(src|chronik)/.*\\.h$)", "exclude": [ "(^src/(crypto/ctaes|secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)", "(^src/tinyformat.h$)" ] }, "lint-include-source": { "type": "lint-include-source", "include": "(^(src|chronik)/.*\\.(h|c|cpp)$)", "exclude": [ "(^src/(secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)" ] }, "lint-std-chrono": { "type": "lint-std-chrono", "include": "(^(src|chronik)/.*\\.(h|cpp)$)" }, "lint-stdint": { "type": "lint-stdint", "include": "(^(src|chronik)/.*\\.(h|c|cpp)$)", "exclude": [ "(^src/(secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)", "(^src/compat/assumptions.h$)" ] }, "lint-source-filename": { "type": "lint-source-filename", "include": "(^(src|chronik)/.*\\.(h|c|cpp)$)", "exclude": [ "(^src/(secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)" ] }, "lint-boost-dependencies": { "type": "lint-boost-dependencies", "include": "(^(src|chronik)/.*\\.(h|cpp)$)" }, "lint-python-encoding": { "type": "lint-python-encoding", "include": "(\\.py$)", "exclude": [ "(^contrib/gitian-builder/)", "(^contrib/apple-sdk-tools/)" ] }, "lint-python-shebang": { "type": "lint-python-shebang", "include": "(\\.py$)", "exclude": [ "(__init__\\.py$)", "(^contrib/gitian-builder/)", "(^contrib/apple-sdk-tools/)" ] }, "lint-bash-shebang": { "type": "lint-bash-shebang", "include": "(\\.sh$)", "exclude": [ "(^contrib/gitian-builder/)" ] }, "shellcheck": { "type": "shellcheck", "version": ">=0.7.0", "flags": [ "--external-sources", "--source-path=SCRIPTDIR" ], "include": "(\\.sh$)", "exclude": [ "(^contrib/gitian-builder/)", "(^src/(secp256k1|univalue)/)" ] }, "lint-shell-locale": { "type": "lint-shell-locale", "include": "(\\.sh$)", "exclude": [ "(^contrib/gitian-builder/)", "(^src/(secp256k1|univalue)/)", "(^cmake/utils/log-and-print-on-failure.sh)" ] }, "lint-cpp-void-parameters": { "type": "lint-cpp-void-parameters", "include": "(^(src|chronik)/.*\\.(h|cpp)$)", "exclude": [ "(^src/(crypto/ctaes|secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)", "(^src/compat/glibc_compat.cpp$)" ] }, "lint-logs": { "type": "lint-logs", "include": "(^(src|chronik)/.*\\.(h|cpp|rs)$)" }, "lint-qt": { "type": "lint-qt", "include": "(^src/qt/.*\\.(h|cpp)$)", "exclude": [ "(^src/qt/(locale|forms|res)/)" ] }, "lint-doxygen": { "type": "lint-doxygen", "include": "(^(src|chronik)/.*\\.(h|c|cpp)$)", "exclude": [ "(^src/(crypto/ctaes|secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)" ] }, "lint-whitespace": { "type": "lint-whitespace", "include": "(\\.(ac|am|cmake|conf|in|include|json|m4|md|openrc|php|pl|rs|sh|txt|yml)$)", "exclude": [ "(^contrib/gitian-builder/)", "(^src/(secp256k1|univalue|leveldb)/)", "(^src/bench/nanobench.h$)" ] }, "yamllint": { "type": "yamllint", "include": "(\\.(yml|yaml)$)", "exclude": "(^src/(secp256k1|univalue|leveldb)/)" }, "lint-check-nonfatal": { "type": "lint-check-nonfatal", "include": [ "(^src/rpc/.*\\.(h|c|cpp)$)", "(^src/wallet/rpc*.*\\.(h|c|cpp)$)" ], "exclude": "(^src/rpc/server.cpp)" }, "lint-markdown": { "type": "lint-markdown", "include": [ "(\\.md$)" ], "exclude": "(^contrib/gitian-builder/)" }, "lint-python-mypy": { "type": "lint-python-mypy", "version": ">=0.910", "include": "(\\.py$)", "exclude": [ "(^contrib/gitian-builder/)", "(^contrib/apple-sdk-tools/)", "(^contrib/macdeploy/)" ], "flags": [ "--ignore-missing-imports", "--install-types", "--non-interactive" ] }, "lint-python-mutable-default": { "type": "lint-python-mutable-default", "include": "(\\.py$)", "exclude": [ "(^contrib/gitian-builder/)", "(^contrib/apple-sdk-tools/)" ] }, "prettier": { "type": "prettier", "version": ">=2.6.0", "include": [ "(^cashtab/.*\\.(css|html|js|json|jsx|md|scss|ts|tsx)$)", "(^web/.*\\.(css|html|js|json|jsx|md|scss|ts|tsx)$)" ], "exclude": "(^web/.*/translations/.*\\.json$)" }, "lint-python-isort": { "type": "lint-python-isort", "version": ">=5.6.4", "include": "(\\.py$)", "exclude": [ "(^contrib/gitian-builder/)", "(^contrib/apple-sdk-tools/)" ] }, "rustfmt": { "type": "rustfmt", "version": ">=1.5.1", "include": "(\\.rs$)" }, "eslint": { "type": "eslint", "version": ">=8.0.0", "include": [ "(cashtab/.*\\.js$)", "(apps/alias-server/.*\\.js$)", "(modules/ecashaddrjs/.*\\.js$)", "(apps/ecash-herald/.*\\.js$)", "(modules/chronik-client/.*\\.(js|jsx|ts|tsx)$)" ] }, "lint-python-flynt": { "type": "lint-python-flynt", "version": ">=0.78", "include": "(\\.py$)", "exclude": [ "(^contrib/gitian-builder/)", "(^contrib/apple-sdk-tools/)" ] } } } diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 65b67b937..739195947 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -1,336 +1,381 @@ #!/usr/bin/env python3 # Copyright (c) 2017-2019 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 bitcoin-cli""" from decimal import Decimal from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_raises_process_error, assert_raises_rpc_error, get_auth_cookie, ) # The block reward of coinbaseoutput.nValue (50) BTC/block matures after # COINBASE_MATURITY (100) blocks. Therefore, after mining 101 blocks we expect # node 0 to have a balance of (BLOCKS - COINBASE_MATURITY) * 50 BTC/block. BLOCKS = 101 BALANCE = (BLOCKS - 100) * 50000000 -JSON_PARSING_ERROR = 'error: Error parsing JSON: foo' -BLOCKS_VALUE_OF_ZERO = 'error: the first argument (number of blocks to generate, default: 1) must be an integer value greater than zero' -TOO_MANY_ARGS = 'error: too many arguments (maximum 2 for nblocks and maxtries)' -WALLET_NOT_LOADED = 'Requested wallet does not exist or is not loaded' -WALLET_NOT_SPECIFIED = 'Wallet file not specified' +JSON_PARSING_ERROR = "error: Error parsing JSON: foo" +BLOCKS_VALUE_OF_ZERO = ( + "error: the first argument (number of blocks to generate, default: 1) must be an" + " integer value greater than zero" +) +TOO_MANY_ARGS = "error: too many arguments (maximum 2 for nblocks and maxtries)" +WALLET_NOT_LOADED = "Requested wallet does not exist or is not loaded" +WALLET_NOT_SPECIFIED = "Wallet file not specified" class TestBitcoinCli(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 def skip_test_if_missing_module(self): self.skip_if_no_cli() def run_test(self): """Main test logic""" self.generate(self.nodes[0], BLOCKS) self.log.info( - "Compare responses from getblockchaininfo RPC and `bitcoin-cli getblockchaininfo`") + "Compare responses from getblockchaininfo RPC and `bitcoin-cli" + " getblockchaininfo`" + ) cli_response = self.nodes[0].cli.getblockchaininfo() rpc_response = self.nodes[0].getblockchaininfo() assert_equal(cli_response, rpc_response) user, password = get_auth_cookie(self.nodes[0].datadir, self.chain) self.log.info("Test -stdinrpcpass option") - assert_equal(BLOCKS, self.nodes[0].cli( - f'-rpcuser={user}', '-stdinrpcpass', cli_input=password).getblockcount()) - assert_raises_process_error(1, "Incorrect rpcuser or rpcpassword", self.nodes[0].cli( - f'-rpcuser={user}', '-stdinrpcpass', cli_input="foo").echo) + assert_equal( + BLOCKS, + self.nodes[0] + .cli(f"-rpcuser={user}", "-stdinrpcpass", cli_input=password) + .getblockcount(), + ) + assert_raises_process_error( + 1, + "Incorrect rpcuser or rpcpassword", + self.nodes[0] + .cli(f"-rpcuser={user}", "-stdinrpcpass", cli_input="foo") + .echo, + ) self.log.info("Test -stdin and -stdinrpcpass") - assert_equal(["foo", "bar"], self.nodes[0].cli(f'-rpcuser={user}', - '-stdin', '-stdinrpcpass', cli_input=f"{password}\nfoo\nbar").echo()) - assert_raises_process_error(1, "Incorrect rpcuser or rpcpassword", self.nodes[0].cli( - f'-rpcuser={user}', '-stdin', '-stdinrpcpass', cli_input="foo").echo) + assert_equal( + ["foo", "bar"], + self.nodes[0] + .cli( + f"-rpcuser={user}", + "-stdin", + "-stdinrpcpass", + cli_input=f"{password}\nfoo\nbar", + ) + .echo(), + ) + assert_raises_process_error( + 1, + "Incorrect rpcuser or rpcpassword", + self.nodes[0] + .cli(f"-rpcuser={user}", "-stdin", "-stdinrpcpass", cli_input="foo") + .echo, + ) self.log.info("Test connecting to a non-existing server") assert_raises_process_error( - 1, "Could not connect to the server", self.nodes[0].cli('-rpcport=1').echo) + 1, "Could not connect to the server", self.nodes[0].cli("-rpcport=1").echo + ) self.log.info("Test connecting with non-existing RPC cookie file") - assert_raises_process_error(1, "Could not locate RPC credentials", self.nodes[0].cli( - '-rpccookiefile=does-not-exist', '-rpcpassword=').echo) + assert_raises_process_error( + 1, + "Could not locate RPC credentials", + self.nodes[0].cli("-rpccookiefile=does-not-exist", "-rpcpassword=").echo, + ) self.log.info("Test -getinfo with arguments fails") assert_raises_process_error( - 1, "-getinfo takes no arguments", self.nodes[0].cli('-getinfo').help) + 1, "-getinfo takes no arguments", self.nodes[0].cli("-getinfo").help + ) - self.log.info( - "Test -getinfo returns expected network and blockchain info") + self.log.info("Test -getinfo returns expected network and blockchain info") if self.is_wallet_compiled(): self.nodes[0].encryptwallet(password) - cli_get_info = self.nodes[0].cli().send_cli('-getinfo') + cli_get_info = self.nodes[0].cli().send_cli("-getinfo") network_info = self.nodes[0].getnetworkinfo() blockchain_info = self.nodes[0].getblockchaininfo() - assert_equal(cli_get_info['version'], network_info['version']) - assert_equal(cli_get_info['blocks'], blockchain_info['blocks']) - assert_equal(cli_get_info['headers'], blockchain_info['headers']) - assert_equal(cli_get_info['timeoffset'], network_info['timeoffset']) + assert_equal(cli_get_info["version"], network_info["version"]) + assert_equal(cli_get_info["blocks"], blockchain_info["blocks"]) + assert_equal(cli_get_info["headers"], blockchain_info["headers"]) + assert_equal(cli_get_info["timeoffset"], network_info["timeoffset"]) assert_equal( - cli_get_info['connections'], + cli_get_info["connections"], { - 'in': network_info['connections_in'], - 'out': network_info['connections_out'], - 'total': network_info['connections'] - } + "in": network_info["connections_in"], + "out": network_info["connections_out"], + "total": network_info["connections"], + }, ) - assert_equal(cli_get_info['proxy'], - network_info['networks'][0]['proxy']) - assert_equal(cli_get_info['difficulty'], blockchain_info['difficulty']) - assert_equal(cli_get_info['chain'], blockchain_info['chain']) + assert_equal(cli_get_info["proxy"], network_info["networks"][0]["proxy"]) + assert_equal(cli_get_info["difficulty"], blockchain_info["difficulty"]) + assert_equal(cli_get_info["chain"], blockchain_info["chain"]) if self.is_wallet_compiled(): self.log.info( - "Test -getinfo and bitcoin-cli getwalletinfo return expected wallet info") - assert_equal(cli_get_info['balance'], BALANCE) - assert 'balances' not in cli_get_info.keys() + "Test -getinfo and bitcoin-cli getwalletinfo return expected wallet" + " info" + ) + assert_equal(cli_get_info["balance"], BALANCE) + assert "balances" not in cli_get_info.keys() wallet_info = self.nodes[0].getwalletinfo() - assert_equal( - cli_get_info['keypoolsize'], - wallet_info['keypoolsize']) - assert_equal( - cli_get_info['unlocked_until'], - wallet_info['unlocked_until']) - assert_equal(cli_get_info['paytxfee'], wallet_info['paytxfee']) - assert_equal(cli_get_info['relayfee'], network_info['relayfee']) + assert_equal(cli_get_info["keypoolsize"], wallet_info["keypoolsize"]) + assert_equal(cli_get_info["unlocked_until"], wallet_info["unlocked_until"]) + assert_equal(cli_get_info["paytxfee"], wallet_info["paytxfee"]) + assert_equal(cli_get_info["relayfee"], network_info["relayfee"]) assert_equal(self.nodes[0].cli.getwalletinfo(), wallet_info) # Setup to test -getinfo, -generate, and -rpcwallet= with multiple # wallets. - wallets = [self.default_wallet_name, 'Encrypted', 'secret'] + wallets = [self.default_wallet_name, "Encrypted", "secret"] amounts = [ - BALANCE + Decimal('9999995.50'), + BALANCE + Decimal("9999995.50"), Decimal(9000000), - Decimal(31000000)] + Decimal(31000000), + ] self.nodes[0].createwallet(wallet_name=wallets[1]) self.nodes[0].createwallet(wallet_name=wallets[2]) w1 = self.nodes[0].get_wallet_rpc(wallets[0]) w2 = self.nodes[0].get_wallet_rpc(wallets[1]) w3 = self.nodes[0].get_wallet_rpc(wallets[2]) - rpcwallet2 = f'-rpcwallet={wallets[1]}' - rpcwallet3 = f'-rpcwallet={wallets[2]}' + rpcwallet2 = f"-rpcwallet={wallets[1]}" + rpcwallet3 = f"-rpcwallet={wallets[2]}" w1.walletpassphrase(password, self.rpc_timeout) w2.encryptwallet(password) w1.sendtoaddress(w2.getnewaddress(), amounts[1]) w1.sendtoaddress(w3.getnewaddress(), amounts[2]) # Mine a block to confirm; adds a block reward (50 BTC) to the # default wallet. self.generate(self.nodes[0], 1) self.log.info( - "Test -getinfo with multiple wallets and -rpcwallet returns specified wallet balance") + "Test -getinfo with multiple wallets and -rpcwallet returns specified" + " wallet balance" + ) for i in range(len(wallets)): - cli_get_info = self.nodes[0].cli( - '-getinfo', f'-rpcwallet={wallets[i]}').send_cli() - assert 'balances' not in cli_get_info.keys() - assert_equal(cli_get_info['balance'], amounts[i]) + cli_get_info = ( + self.nodes[0].cli("-getinfo", f"-rpcwallet={wallets[i]}").send_cli() + ) + assert "balances" not in cli_get_info.keys() + assert_equal(cli_get_info["balance"], amounts[i]) self.log.info( "Test -getinfo with multiple wallets and " - "-rpcwallet=non-existing-wallet returns no balances") - cli_get_info_keys = self.nodes[0].cli( - '-getinfo', '-rpcwallet=does-not-exist').send_cli().keys() - assert 'balance' not in cli_get_info_keys - assert 'balances' not in cli_get_info_keys + "-rpcwallet=non-existing-wallet returns no balances" + ) + cli_get_info_keys = ( + self.nodes[0] + .cli("-getinfo", "-rpcwallet=does-not-exist") + .send_cli() + .keys() + ) + assert "balance" not in cli_get_info_keys + assert "balances" not in cli_get_info_keys self.log.info( "Test -getinfo with multiple wallets returns all loaded " - "wallet names and balances") + "wallet names and balances" + ) assert_equal(set(self.nodes[0].listwallets()), set(wallets)) - cli_get_info = self.nodes[0].cli('-getinfo').send_cli() - assert 'balance' not in cli_get_info.keys() - assert_equal(cli_get_info['balances'], dict(zip(wallets, amounts))) + cli_get_info = self.nodes[0].cli("-getinfo").send_cli() + assert "balance" not in cli_get_info.keys() + assert_equal(cli_get_info["balances"], dict(zip(wallets, amounts))) # Unload the default wallet and re-verify. self.nodes[0].unloadwallet(wallets[0]) assert wallets[0] not in self.nodes[0].listwallets() - cli_get_info = self.nodes[0].cli('-getinfo').send_cli() - assert 'balance' not in cli_get_info.keys() - assert_equal(cli_get_info['balances'], - dict(zip(wallets[1:], amounts[1:]))) + cli_get_info = self.nodes[0].cli("-getinfo").send_cli() + assert "balance" not in cli_get_info.keys() + assert_equal(cli_get_info["balances"], dict(zip(wallets[1:], amounts[1:]))) self.log.info( "Test -getinfo after unloading all wallets except a " - "non-default one returns its balance") + "non-default one returns its balance" + ) self.nodes[0].unloadwallet(wallets[2]) assert_equal(self.nodes[0].listwallets(), [wallets[1]]) - cli_get_info = self.nodes[0].cli('-getinfo').send_cli() - assert 'balances' not in cli_get_info.keys() - assert_equal(cli_get_info['balance'], amounts[1]) + cli_get_info = self.nodes[0].cli("-getinfo").send_cli() + assert "balances" not in cli_get_info.keys() + assert_equal(cli_get_info["balance"], amounts[1]) self.log.info( "Test -getinfo with -rpcwallet=remaining-non-default-wallet" - " returns only its balance") - cli_get_info = self.nodes[0].cli('-getinfo', rpcwallet2).send_cli() - assert 'balances' not in cli_get_info.keys() - assert_equal(cli_get_info['balance'], amounts[1]) + " returns only its balance" + ) + cli_get_info = self.nodes[0].cli("-getinfo", rpcwallet2).send_cli() + assert "balances" not in cli_get_info.keys() + assert_equal(cli_get_info["balance"], amounts[1]) self.log.info( - "Test -getinfo with -rpcwallet=unloaded wallet returns" - " no balances") - cli_get_info_keys = self.nodes[0].cli( - '-getinfo', rpcwallet3).send_cli().keys() - assert 'balance' not in cli_get_info_keys - assert 'balances' not in cli_get_info_keys + "Test -getinfo with -rpcwallet=unloaded wallet returns no balances" + ) + cli_get_info_keys = ( + self.nodes[0].cli("-getinfo", rpcwallet3).send_cli().keys() + ) + assert "balance" not in cli_get_info_keys + assert "balances" not in cli_get_info_keys # Test bitcoin-cli -generate. n1 = 3 n2 = 4 w2.walletpassphrase(password, self.rpc_timeout) blocks = self.nodes[0].getblockcount() - self.log.info('Test -generate with no args') - generate = self.nodes[0].cli('-generate').send_cli() - assert_equal(set(generate.keys()), {'address', 'blocks'}) + self.log.info("Test -generate with no args") + generate = self.nodes[0].cli("-generate").send_cli() + assert_equal(set(generate.keys()), {"address", "blocks"}) assert_equal(len(generate["blocks"]), 1) assert_equal(self.nodes[0].getblockcount(), blocks + 1) - self.log.info('Test -generate with bad args') + self.log.info("Test -generate with bad args") assert_raises_process_error( - 1, JSON_PARSING_ERROR, self.nodes[0].cli( - '-generate', 'foo').echo) + 1, JSON_PARSING_ERROR, self.nodes[0].cli("-generate", "foo").echo + ) assert_raises_process_error( - 1, BLOCKS_VALUE_OF_ZERO, self.nodes[0].cli( - '-generate', 0).echo) + 1, BLOCKS_VALUE_OF_ZERO, self.nodes[0].cli("-generate", 0).echo + ) assert_raises_process_error( - 1, TOO_MANY_ARGS, self.nodes[0].cli( - '-generate', 1, 2, 3).echo) + 1, TOO_MANY_ARGS, self.nodes[0].cli("-generate", 1, 2, 3).echo + ) - self.log.info('Test -generate with nblocks') - generate = self.nodes[0].cli('-generate', n1).send_cli() - assert_equal(set(generate.keys()), {'address', 'blocks'}) + self.log.info("Test -generate with nblocks") + generate = self.nodes[0].cli("-generate", n1).send_cli() + assert_equal(set(generate.keys()), {"address", "blocks"}) assert_equal(len(generate["blocks"]), n1) assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n1) - self.log.info('Test -generate with nblocks and maxtries') - generate = self.nodes[0].cli('-generate', n2, 1000000).send_cli() - assert_equal(set(generate.keys()), {'address', 'blocks'}) + self.log.info("Test -generate with nblocks and maxtries") + generate = self.nodes[0].cli("-generate", n2, 1000000).send_cli() + assert_equal(set(generate.keys()), {"address", "blocks"}) assert_equal(len(generate["blocks"]), n2) assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n1 + n2) - self.log.info('Test -generate -rpcwallet in single-wallet mode') - generate = self.nodes[0].cli(rpcwallet2, '-generate').send_cli() - assert_equal(set(generate.keys()), {'address', 'blocks'}) + self.log.info("Test -generate -rpcwallet in single-wallet mode") + generate = self.nodes[0].cli(rpcwallet2, "-generate").send_cli() + assert_equal(set(generate.keys()), {"address", "blocks"}) assert_equal(len(generate["blocks"]), 1) assert_equal(self.nodes[0].getblockcount(), blocks + 2 + n1 + n2) - self.log.info( - 'Test -generate -rpcwallet=unloaded wallet raises RPC error') - assert_raises_rpc_error(-18, - WALLET_NOT_LOADED, - self.nodes[0].cli(rpcwallet3, - '-generate').echo) - assert_raises_rpc_error(-18, - WALLET_NOT_LOADED, - self.nodes[0].cli(rpcwallet3, - '-generate', - 'foo').echo) - assert_raises_rpc_error(-18, - WALLET_NOT_LOADED, - self.nodes[0].cli(rpcwallet3, - '-generate', - 0).echo) - assert_raises_rpc_error(-18, - WALLET_NOT_LOADED, - self.nodes[0].cli(rpcwallet3, - '-generate', - 1, - 2, - 3).echo) + self.log.info("Test -generate -rpcwallet=unloaded wallet raises RPC error") + assert_raises_rpc_error( + -18, WALLET_NOT_LOADED, self.nodes[0].cli(rpcwallet3, "-generate").echo + ) + assert_raises_rpc_error( + -18, + WALLET_NOT_LOADED, + self.nodes[0].cli(rpcwallet3, "-generate", "foo").echo, + ) + assert_raises_rpc_error( + -18, + WALLET_NOT_LOADED, + self.nodes[0].cli(rpcwallet3, "-generate", 0).echo, + ) + assert_raises_rpc_error( + -18, + WALLET_NOT_LOADED, + self.nodes[0].cli(rpcwallet3, "-generate", 1, 2, 3).echo, + ) # Test bitcoin-cli -generate with -rpcwallet in multiwallet mode. self.nodes[0].loadwallet(wallets[2]) n3 = 4 n4 = 10 blocks = self.nodes[0].getblockcount() - self.log.info('Test -generate -rpcwallet with no args') - generate = self.nodes[0].cli(rpcwallet2, '-generate').send_cli() - assert_equal(set(generate.keys()), {'address', 'blocks'}) + self.log.info("Test -generate -rpcwallet with no args") + generate = self.nodes[0].cli(rpcwallet2, "-generate").send_cli() + assert_equal(set(generate.keys()), {"address", "blocks"}) assert_equal(len(generate["blocks"]), 1) assert_equal(self.nodes[0].getblockcount(), blocks + 1) - self.log.info('Test -generate -rpcwallet with bad args') + self.log.info("Test -generate -rpcwallet with bad args") assert_raises_process_error( - 1, JSON_PARSING_ERROR, self.nodes[0].cli( - rpcwallet2, '-generate', 'foo').echo) + 1, + JSON_PARSING_ERROR, + self.nodes[0].cli(rpcwallet2, "-generate", "foo").echo, + ) assert_raises_process_error( - 1, BLOCKS_VALUE_OF_ZERO, self.nodes[0].cli( - rpcwallet2, '-generate', 0).echo) + 1, + BLOCKS_VALUE_OF_ZERO, + self.nodes[0].cli(rpcwallet2, "-generate", 0).echo, + ) assert_raises_process_error( - 1, TOO_MANY_ARGS, self.nodes[0].cli( - rpcwallet2, '-generate', 1, 2, 3).echo) - - self.log.info('Test -generate -rpcwallet with nblocks') - generate = self.nodes[0].cli( - rpcwallet2, '-generate', n3).send_cli() - assert_equal(set(generate.keys()), {'address', 'blocks'}) + 1, + TOO_MANY_ARGS, + self.nodes[0].cli(rpcwallet2, "-generate", 1, 2, 3).echo, + ) + + self.log.info("Test -generate -rpcwallet with nblocks") + generate = self.nodes[0].cli(rpcwallet2, "-generate", n3).send_cli() + assert_equal(set(generate.keys()), {"address", "blocks"}) assert_equal(len(generate["blocks"]), n3) assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n3) - self.log.info( - 'Test -generate -rpcwallet with nblocks and maxtries') - generate = self.nodes[0].cli( - rpcwallet2, '-generate', n4, 1000000).send_cli() - assert_equal(set(generate.keys()), {'address', 'blocks'}) + self.log.info("Test -generate -rpcwallet with nblocks and maxtries") + generate = ( + self.nodes[0].cli(rpcwallet2, "-generate", n4, 1000000).send_cli() + ) + assert_equal(set(generate.keys()), {"address", "blocks"}) assert_equal(len(generate["blocks"]), n4) assert_equal(self.nodes[0].getblockcount(), blocks + 1 + n3 + n4) self.log.info( - 'Test -generate without -rpcwallet in multiwallet mode raises RPC error') - assert_raises_rpc_error(-19, - WALLET_NOT_SPECIFIED, - self.nodes[0].cli('-generate').echo) - assert_raises_rpc_error(-19, - WALLET_NOT_SPECIFIED, - self.nodes[0].cli('-generate', - 'foo').echo) - assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, - self.nodes[0].cli('-generate', 0).echo) - assert_raises_rpc_error(-19, WALLET_NOT_SPECIFIED, - self.nodes[0].cli('-generate', 1, 2, 3).echo) + "Test -generate without -rpcwallet in multiwallet mode raises RPC error" + ) + assert_raises_rpc_error( + -19, WALLET_NOT_SPECIFIED, self.nodes[0].cli("-generate").echo + ) + assert_raises_rpc_error( + -19, WALLET_NOT_SPECIFIED, self.nodes[0].cli("-generate", "foo").echo + ) + assert_raises_rpc_error( + -19, WALLET_NOT_SPECIFIED, self.nodes[0].cli("-generate", 0).echo + ) + assert_raises_rpc_error( + -19, WALLET_NOT_SPECIFIED, self.nodes[0].cli("-generate", 1, 2, 3).echo + ) else: self.log.info( - "*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests skipped") + "*** Wallet not compiled; cli getwalletinfo and -getinfo wallet tests" + " skipped" + ) # maintain block parity with the wallet_compiled conditional branch self.generate(self.nodes[0], 25) self.log.info("Test -version with node stopped") self.stop_node(0) - cli_response = self.nodes[0].cli().send_cli('-version') + cli_response = self.nodes[0].cli().send_cli("-version") assert ( f"{self.config['environment']['PACKAGE_NAME']} RPC client version" in cli_response ) - self.log.info( - "Test -rpcwait option successfully waits for RPC connection") + self.log.info("Test -rpcwait option successfully waits for RPC connection") # Start node without RPC connection. self.nodes[0].start() # ensure cookie file is available to avoid race condition self.nodes[0].wait_for_cookie_credentials() - blocks = self.nodes[0].cli('-rpcwait').send_cli('getblockcount') + blocks = self.nodes[0].cli("-rpcwait").send_cli("getblockcount") self.nodes[0].wait_for_rpc_connection() assert_equal(blocks, BLOCKS + 25) -if __name__ == '__main__': +if __name__ == "__main__": TestBitcoinCli().main() diff --git a/test/functional/interface_http.py b/test/functional/interface_http.py index 6de84b3e2..5dbbc0f66 100755 --- a/test/functional/interface_http.py +++ b/test/functional/interface_http.py @@ -1,173 +1,178 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 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 the RPC HTTP basics.""" import http.client import urllib.parse from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, str_to_b64str -class HTTPBasicsTest (BitcoinTestFramework): +class HTTPBasicsTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 3 self.supports_cli = False def setup_network(self): self.extra_args = [["-rpccorsdomain=null"], [], []] self.setup_nodes() def run_test(self): - # # lowlevel check for http persistent connection # # url = urllib.parse.urlparse(self.nodes[0].url) authpair = f"{url.username}:{url.password}" headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() - conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) + conn.request("POST", "/", '{"method": "getbestblockhash"}', headers) out1 = conn.getresponse().read() assert b'"error":null' in out1 assert conn.sock is not None # according to http/1.1 connection must still be open! # send 2nd request without closing connection - conn.request('POST', '/', '{"method": "getchaintips"}', headers) + conn.request("POST", "/", '{"method": "getchaintips"}', headers) out1 = conn.getresponse().read() assert b'"error":null' in out1 # must also response with a correct json-rpc message assert conn.sock is not None # according to http/1.1 connection must still be open! conn.close() # same should be if we add keep-alive because this should be the std. # behaviour - headers = {"Authorization": "Basic " + - str_to_b64str(authpair), "Connection": "keep-alive"} + headers = { + "Authorization": "Basic " + str_to_b64str(authpair), + "Connection": "keep-alive", + } conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() - conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) + conn.request("POST", "/", '{"method": "getbestblockhash"}', headers) out1 = conn.getresponse().read() assert b'"error":null' in out1 assert conn.sock is not None # according to http/1.1 connection must still be open! # send 2nd request without closing connection - conn.request('POST', '/', '{"method": "getchaintips"}', headers) + conn.request("POST", "/", '{"method": "getchaintips"}', headers) out1 = conn.getresponse().read() assert b'"error":null' in out1 # must also response with a correct json-rpc message assert conn.sock is not None # according to http/1.1 connection must still be open! conn.close() # now do the same with "Connection: close" - headers = {"Authorization": "Basic " + - str_to_b64str(authpair), "Connection": "close"} + headers = { + "Authorization": "Basic " + str_to_b64str(authpair), + "Connection": "close", + } conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() - conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) + conn.request("POST", "/", '{"method": "getbestblockhash"}', headers) out1 = conn.getresponse().read() assert b'"error":null' in out1 assert conn.sock is None # now the connection must be closed after the response # node1 (2nd node) is running with disabled keep-alive option urlNode1 = urllib.parse.urlparse(self.nodes[1].url) authpair = f"{urlNode1.username}:{urlNode1.password}" headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} conn = http.client.HTTPConnection(urlNode1.hostname, urlNode1.port) conn.connect() - conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) + conn.request("POST", "/", '{"method": "getbestblockhash"}', headers) out1 = conn.getresponse().read() assert b'"error":null' in out1 # node2 (third node) is running with standard keep-alive parameters # which means keep-alive is on urlNode2 = urllib.parse.urlparse(self.nodes[2].url) authpair = f"{urlNode2.username}:{urlNode2.password}" headers = {"Authorization": f"Basic {str_to_b64str(authpair)}"} conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() - conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) + conn.request("POST", "/", '{"method": "getbestblockhash"}', headers) out1 = conn.getresponse().read() assert b'"error":null' in out1 assert conn.sock is not None # connection must be closed because bitcoind should use # keep-alive by default # Check excessive request size conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() - conn.request('GET', f"/{'x' * 1000}", '', headers) + conn.request("GET", f"/{'x' * 1000}", "", headers) out1 = conn.getresponse() assert_equal(out1.status, http.client.NOT_FOUND) conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() - conn.request('GET', f"/{'x' * 10000}", '', headers) + conn.request("GET", f"/{'x' * 10000}", "", headers) out1 = conn.getresponse() assert_equal(out1.status, http.client.BAD_REQUEST) # Check Standard CORS request origin = "null" conn = http.client.HTTPConnection(url.hostname, url.port) conn.connect() authpair = f"{url.username}:{url.password}" - headers = {"Authorization": f"Basic {str_to_b64str(authpair)}", - "Origin": origin} - conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) + headers = { + "Authorization": f"Basic {str_to_b64str(authpair)}", + "Origin": origin, + } + conn.request("POST", "/", '{"method": "getbestblockhash"}', headers) out1 = conn.getresponse() assert_equal(out1.status, http.client.OK) assert_equal(out1.headers["Access-Control-Allow-Origin"], origin) assert_equal(out1.headers["Access-Control-Allow-Credentials"], "true") - assert_equal(out1.headers["Access-Control-Expose-Headers"], - "WWW-Authenticate") + assert_equal(out1.headers["Access-Control-Expose-Headers"], "WWW-Authenticate") assert b'"error":null' in out1.read() # Check Pre-flight CORS request - corsheaders = {"Origin": origin, - "Access-Control-Request-Method": "POST"} - conn.request('OPTIONS', '/', None, corsheaders) + corsheaders = {"Origin": origin, "Access-Control-Request-Method": "POST"} + conn.request("OPTIONS", "/", None, corsheaders) out1 = conn.getresponse() assert_equal(out1.status, http.client.OK) assert_equal(out1.headers["Access-Control-Allow-Origin"], origin) assert_equal(out1.headers["Access-Control-Allow-Credentials"], "true") assert_equal(out1.headers["Access-Control-Allow-Methods"], "POST") - assert_equal(out1.headers["Access-Control-Allow-Headers"], - "authorization,content-type") - assert_equal(b'', out1.read()) + assert_equal( + out1.headers["Access-Control-Allow-Headers"], "authorization,content-type" + ) + assert_equal(b"", out1.read()) # Check Standard CORS request to node without CORS, expected failure conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) conn.connect() authpair = f"{url.username}:{url.password}" - headers = {"Authorization": f"Basic {str_to_b64str(authpair)}", - "Origin": origin} - conn.request('POST', '/', '{"method": "getbestblockhash"}', headers) + headers = { + "Authorization": f"Basic {str_to_b64str(authpair)}", + "Origin": origin, + } + conn.request("POST", "/", '{"method": "getbestblockhash"}', headers) out1 = conn.getresponse() assert_equal(out1.status, http.client.UNAUTHORIZED) - assert_equal(b'', out1.read()) + assert_equal(b"", out1.read()) # Check Pre-flight CORS request to node without CORS, expected failure - corsheaders = {"Origin": origin, - "Access-Control-Request-Method": "POST"} - conn.request('OPTIONS', '/', None, corsheaders) + corsheaders = {"Origin": origin, "Access-Control-Request-Method": "POST"} + conn.request("OPTIONS", "/", None, corsheaders) out1 = conn.getresponse() assert_equal(out1.status, http.client.METHOD_NOT_ALLOWED) - assert_equal(b'JSONRPC server handles only POST requests', out1.read()) + assert_equal(b"JSONRPC server handles only POST requests", out1.read()) -if __name__ == '__main__': +if __name__ == "__main__": HTTPBasicsTest().main() diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index 7157fdbc5..9a19f5e34 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -1,389 +1,437 @@ #!/usr/bin/env python3 # Copyright (c) 2014-2019 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 the REST API.""" import http.client import json import urllib.parse from decimal import Decimal from enum import Enum from io import BytesIO from struct import pack, unpack from test_framework.messages import BLOCK_HEADER_SIZE from test_framework.test_framework import BitcoinTestFramework from test_framework.util import ( assert_equal, assert_greater_than, assert_greater_than_or_equal, ) class ReqType(Enum): JSON = 1 BIN = 2 HEX = 3 class RetType(Enum): OBJ = 1 BYTES = 2 JSON = 3 def filter_output_indices_by_value(vouts, value): for vout in vouts: - if vout['value'] == value: - yield vout['n'] + if vout["value"] == value: + yield vout["n"] -class RESTTest (BitcoinTestFramework): +class RESTTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 2 self.extra_args = [["-rest"], []] self.supports_cli = False def skip_test_if_missing_module(self): self.skip_if_no_wallet() - def test_rest_request(self, uri, http_method='GET', req_type=ReqType.JSON, - body='', status=200, ret_type=RetType.JSON): + def test_rest_request( + self, + uri, + http_method="GET", + req_type=ReqType.JSON, + body="", + status=200, + ret_type=RetType.JSON, + ): rest_uri = f"/rest{uri}" if req_type == ReqType.JSON: - rest_uri += '.json' + rest_uri += ".json" elif req_type == ReqType.BIN: - rest_uri += '.bin' + rest_uri += ".bin" elif req_type == ReqType.HEX: - rest_uri += '.hex' + rest_uri += ".hex" conn = http.client.HTTPConnection(self.url.hostname, self.url.port) - self.log.debug(f'{http_method} {rest_uri} {body}') - if http_method == 'GET': - conn.request('GET', rest_uri) - elif http_method == 'POST': - conn.request('POST', rest_uri, body) + self.log.debug(f"{http_method} {rest_uri} {body}") + if http_method == "GET": + conn.request("GET", rest_uri) + elif http_method == "POST": + conn.request("POST", rest_uri, body) resp = conn.getresponse() assert_equal(resp.status, status) if ret_type == RetType.OBJ: return resp elif ret_type == RetType.BYTES: return resp.read() elif ret_type == RetType.JSON: - return json.loads(resp.read().decode('utf-8'), parse_float=Decimal) + return json.loads(resp.read().decode("utf-8"), parse_float=Decimal) def run_test(self): self.url = urllib.parse.urlparse(self.nodes[0].url) self.log.info("Mine blocks and send Bitcoin Cash to node 1") # Random address so node1's balance doesn't increase not_related_address = "2MxqoHEdNQTyYeX1mHcbrrpzgojbosTpCvJ" self.generate(self.nodes[0], 1) self.generatetoaddress(self.nodes[1], 100, not_related_address) assert_equal(self.nodes[0].getbalance(), 50000000) - txid = self.nodes[0].sendtoaddress( - self.nodes[1].getnewaddress(), 100000) + txid = self.nodes[0].sendtoaddress(self.nodes[1].getnewaddress(), 100000) self.sync_all() self.log.info("Test the /tx URI") json_obj = self.test_rest_request(f"/tx/{txid}") - assert_equal(json_obj['txid'], txid) + assert_equal(json_obj["txid"], txid) # Check hex format response hex_response = self.test_rest_request( - f"/tx/{txid}", req_type=ReqType.HEX, ret_type=RetType.OBJ) - assert_greater_than_or_equal(int(hex_response.getheader('content-length')), - json_obj['size'] * 2) + f"/tx/{txid}", req_type=ReqType.HEX, ret_type=RetType.OBJ + ) + assert_greater_than_or_equal( + int(hex_response.getheader("content-length")), json_obj["size"] * 2 + ) # Get the vin to later check for utxo (should be spent by then) - spent = (json_obj['vin'][0]['txid'], json_obj['vin'][0]['vout']) + spent = (json_obj["vin"][0]["txid"], json_obj["vin"][0]["vout"]) # Get n of 100_000 XEC outpoint - n, = filter_output_indices_by_value(json_obj['vout'], Decimal('100000')) + (n,) = filter_output_indices_by_value(json_obj["vout"], Decimal("100000")) spending = (txid, n) self.log.info("Query an unspent TXO using the /getutxos URI") self.generatetoaddress(self.nodes[1], 1, not_related_address) bb_hash = self.nodes[0].getbestblockhash() assert_equal(self.nodes[1].getbalance(), Decimal("100000")) # Check chainTip response json_obj = self.test_rest_request(f"/getutxos/{txid}-{n}") - assert_equal(json_obj['chaintipHash'], bb_hash) + assert_equal(json_obj["chaintipHash"], bb_hash) # Make sure there is one utxo - assert_equal(len(json_obj['utxos']), 1) - assert_equal(json_obj['utxos'][0]['value'], Decimal('100000')) + assert_equal(len(json_obj["utxos"]), 1) + assert_equal(json_obj["utxos"][0]["value"], Decimal("100000")) self.log.info("Query a spent TXO using the /getutxos URI") json_obj = self.test_rest_request(f"/getutxos/{spent[0]}-{spent[1]}") # Check chainTip response - assert_equal(json_obj['chaintipHash'], bb_hash) + assert_equal(json_obj["chaintipHash"], bb_hash) # Make sure there is no utxo in the response because this outpoint has # been spent - assert_equal(len(json_obj['utxos']), 0) + assert_equal(len(json_obj["utxos"]), 0) # Check bitmap - assert_equal(json_obj['bitmap'], "0") + assert_equal(json_obj["bitmap"], "0") self.log.info("Query two TXOs using the /getutxos URI") - json_obj = self.test_rest_request( - f"/getutxos/{txid}-{n}/{spent[0]}-{spent[1]}") + json_obj = self.test_rest_request(f"/getutxos/{txid}-{n}/{spent[0]}-{spent[1]}") - assert_equal(len(json_obj['utxos']), 1) - assert_equal(json_obj['bitmap'], "10") + assert_equal(len(json_obj["utxos"]), 1) + assert_equal(json_obj["bitmap"], "10") - self.log.info( - "Query the TXOs using the /getutxos URI with a binary response") + self.log.info("Query the TXOs using the /getutxos URI with a binary response") - bin_request = b'\x01\x02' + bin_request = b"\x01\x02" for txid, n in [spending, spent]: bin_request += bytes.fromhex(txid) bin_request += pack("i", n) bin_response = self.test_rest_request( - "/getutxos", http_method='POST', req_type=ReqType.BIN, body=bin_request, ret_type=RetType.BYTES) + "/getutxos", + http_method="POST", + req_type=ReqType.BIN, + body=bin_request, + ret_type=RetType.BYTES, + ) output = BytesIO(bin_response) - chain_height, = unpack(" #define MAX_PEER_ADDR_LENGTH {MAX_PEER_ADDR_LENGTH} #define MAX_PEER_CONN_TYPE_LENGTH {MAX_PEER_CONN_TYPE_LENGTH} #define MAX_MSG_TYPE_LENGTH {MAX_MSG_TYPE_LENGTH} #define MAX_MSG_DATA_LENGTH {MAX_MSG_DATA_LENGTH} #define MIN(a,b) ({{ __typeof__ (a) _a = (a); __typeof__ (b) _b = (b); _a < _b ? _a : _b; }}) struct p2p_message {{ u64 peer_id; char peer_addr[MAX_PEER_ADDR_LENGTH]; char peer_conn_type[MAX_PEER_CONN_TYPE_LENGTH]; char msg_type[MAX_MSG_TYPE_LENGTH]; u64 msg_size; u8 msg[MAX_MSG_DATA_LENGTH]; }}; BPF_PERF_OUTPUT(inbound_messages); int trace_inbound_message(struct pt_regs *ctx) {{ struct p2p_message msg = {{}}; bpf_usdt_readarg(1, ctx, &msg.peer_id); bpf_usdt_readarg_p(2, ctx, &msg.peer_addr, MAX_PEER_ADDR_LENGTH); bpf_usdt_readarg_p(3, ctx, &msg.peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH); bpf_usdt_readarg_p(4, ctx, &msg.msg_type, MAX_MSG_TYPE_LENGTH); bpf_usdt_readarg(5, ctx, &msg.msg_size); bpf_usdt_readarg_p(6, ctx, &msg.msg, MIN(msg.msg_size, MAX_MSG_DATA_LENGTH)); inbound_messages.perf_submit(ctx, &msg, sizeof(msg)); return 0; }} BPF_PERF_OUTPUT(outbound_messages); int trace_outbound_message(struct pt_regs *ctx) {{ struct p2p_message msg = {{}}; bpf_usdt_readarg(1, ctx, &msg.peer_id); bpf_usdt_readarg_p(2, ctx, &msg.peer_addr, MAX_PEER_ADDR_LENGTH); bpf_usdt_readarg_p(3, ctx, &msg.peer_conn_type, MAX_PEER_CONN_TYPE_LENGTH); bpf_usdt_readarg_p(4, ctx, &msg.msg_type, MAX_MSG_TYPE_LENGTH); bpf_usdt_readarg(5, ctx, &msg.msg_size); bpf_usdt_readarg_p(6, ctx, &msg.msg, MIN(msg.msg_size, MAX_MSG_DATA_LENGTH)); outbound_messages.perf_submit(ctx, &msg, sizeof(msg)); return 0; }}; """ class NetTracepointTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 def skip_test_if_missing_module(self): self.skip_if_platform_not_linux() self.skip_if_no_bitcoind_tracepoints() self.skip_if_no_python_bcc() self.skip_if_no_bpf_permissions() def run_test(self): # Tests the net:inbound_message and net:outbound_message tracepoints # See # https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-net class P2PMessage(ctypes.Structure): _fields_ = [ ("peer_id", ctypes.c_uint64), ("peer_addr", ctypes.c_char * MAX_PEER_ADDR_LENGTH), ("peer_conn_type", ctypes.c_char * MAX_PEER_CONN_TYPE_LENGTH), ("msg_type", ctypes.c_char * MAX_MSG_TYPE_LENGTH), ("msg_size", ctypes.c_uint64), ("msg", ctypes.c_ubyte * MAX_MSG_DATA_LENGTH), ] def __repr__(self): - return f"P2PMessage(peer={self.peer_id}, addr={self.peer_addr.decode('utf-8')}, conn_type={self.peer_conn_type.decode('utf-8')}, msg_type={self.msg_type.decode('utf-8')}, msg_size={self.msg_size})" + return ( + f"P2PMessage(peer={self.peer_id}," + f" addr={self.peer_addr.decode('utf-8')}," + f" conn_type={self.peer_conn_type.decode('utf-8')}," + f" msg_type={self.msg_type.decode('utf-8')}," + f" msg_size={self.msg_size})" + ) self.log.info( - "hook into the net:inbound_message and net:outbound_message tracepoints") + "hook into the net:inbound_message and net:outbound_message tracepoints" + ) ctx = USDT(pid=self.nodes[0].process.pid) - ctx.enable_probe(probe="net:inbound_message", - fn_name="trace_inbound_message") - ctx.enable_probe(probe="net:outbound_message", - fn_name="trace_outbound_message") + ctx.enable_probe(probe="net:inbound_message", fn_name="trace_inbound_message") + ctx.enable_probe(probe="net:outbound_message", fn_name="trace_outbound_message") bpf = BPF(text=net_tracepoints_program, usdt_contexts=[ctx], debug=0) # The handle_* function is a ctypes callback function called from C. When # we assert in the handle_* function, the AssertError doesn't propagate # back to Python. The exception is ignored. We manually count and assert # that the handle_* functions succeeded. EXPECTED_INOUTBOUND_VERSION_MSG = 1 checked_inbound_version_msg = 0 checked_outbound_version_msg = 0 def check_p2p_message(event, inbound): nonlocal checked_inbound_version_msg, checked_outbound_version_msg if event.msg_type.decode("utf-8") == "version": self.log.info( - f"check_p2p_message(): {'inbound' if inbound else 'outbound'} {event}") + "check_p2p_message():" + f" {'inbound' if inbound else 'outbound'} {event}" + ) peer = self.nodes[0].getpeerinfo()[0] msg = msg_version() - msg.deserialize(BytesIO(bytes(event.msg[:event.msg_size]))) + msg.deserialize(BytesIO(bytes(event.msg[: event.msg_size]))) assert_equal(peer["id"], event.peer_id, peer["id"]) assert_equal(peer["addr"], event.peer_addr.decode("utf-8")) - assert_equal(peer["connection_type"], - event.peer_conn_type.decode("utf-8")) + assert_equal( + peer["connection_type"], event.peer_conn_type.decode("utf-8") + ) if inbound: checked_inbound_version_msg += 1 else: checked_outbound_version_msg += 1 def handle_inbound(_, data, __): event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents check_p2p_message(event, True) def handle_outbound(_, data, __): event = ctypes.cast(data, ctypes.POINTER(P2PMessage)).contents check_p2p_message(event, False) bpf["inbound_messages"].open_perf_buffer(handle_inbound) bpf["outbound_messages"].open_perf_buffer(handle_outbound) self.log.info("connect a P2P test node to our bitcoind node") test_node = P2PInterface() self.nodes[0].add_p2p_connection(test_node) bpf.perf_buffer_poll(timeout=200) - self.log.info( - "check that we got both an inbound and outbound version message") - assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG, - checked_inbound_version_msg) - assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG, - checked_outbound_version_msg) + self.log.info("check that we got both an inbound and outbound version message") + assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG, checked_inbound_version_msg) + assert_equal(EXPECTED_INOUTBOUND_VERSION_MSG, checked_outbound_version_msg) bpf.cleanup() -if __name__ == '__main__': +if __name__ == "__main__": NetTracepointTest().main() diff --git a/test/functional/interface_usdt_utxocache.py b/test/functional/interface_usdt_utxocache.py index bc368c983..1c46b89cc 100755 --- a/test/functional/interface_usdt_utxocache.py +++ b/test/functional/interface_usdt_utxocache.py @@ -1,423 +1,438 @@ #!/usr/bin/env python3 # Copyright (c) 2022 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Tests the utxocache:* tracepoint API interface. https://github.com/Bitcoin-ABC/bitcoin-abc/blob/master/doc/tracing.md#context-utxocache """ import ctypes # Test will be skipped if we don't have bcc installed try: from bcc import BPF, USDT # type: ignore[import] except ImportError: pass from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal from test_framework.wallet import MiniWallet # MAX_MONEY as a string is "21000000000000.00 XEC" MAX_AMOUNT_LENGTH = 22 utxocache_changes_program = """ #include typedef signed long long i64; #define MAX_AMOUNT_LENGTH 22 struct utxocache_change { char txid[32]; u32 index; u32 height; char value[MAX_AMOUNT_LENGTH]; bool is_coinbase; }; BPF_PERF_OUTPUT(utxocache_add); int trace_utxocache_add(struct pt_regs *ctx) { struct utxocache_change add = {}; bpf_usdt_readarg_p(1, ctx, &add.txid, 32); bpf_usdt_readarg(2, ctx, &add.index); bpf_usdt_readarg(3, ctx, &add.height); bpf_usdt_readarg_p(4, ctx, &add.value, MAX_AMOUNT_LENGTH); bpf_usdt_readarg(5, ctx, &add.is_coinbase); utxocache_add.perf_submit(ctx, &add, sizeof(add)); return 0; } BPF_PERF_OUTPUT(utxocache_spent); int trace_utxocache_spent(struct pt_regs *ctx) { struct utxocache_change spent = {}; bpf_usdt_readarg_p(1, ctx, &spent.txid, 32); bpf_usdt_readarg(2, ctx, &spent.index); bpf_usdt_readarg(3, ctx, &spent.height); bpf_usdt_readarg_p(4, ctx, &spent.value, MAX_AMOUNT_LENGTH); bpf_usdt_readarg(5, ctx, &spent.is_coinbase); utxocache_spent.perf_submit(ctx, &spent, sizeof(spent)); return 0; } BPF_PERF_OUTPUT(utxocache_uncache); int trace_utxocache_uncache(struct pt_regs *ctx) { struct utxocache_change uncache = {}; bpf_usdt_readarg_p(1, ctx, &uncache.txid, 32); bpf_usdt_readarg(2, ctx, &uncache.index); bpf_usdt_readarg(3, ctx, &uncache.height); bpf_usdt_readarg_p(4, ctx, &uncache.value, MAX_AMOUNT_LENGTH); bpf_usdt_readarg(5, ctx, &uncache.is_coinbase); utxocache_uncache.perf_submit(ctx, &uncache, sizeof(uncache)); return 0; } """ utxocache_flushes_program = """ #include typedef signed long long i64; struct utxocache_flush { i64 duration; u32 mode; u64 size; u64 memory; bool for_prune; }; BPF_PERF_OUTPUT(utxocache_flush); int trace_utxocache_flush(struct pt_regs *ctx) { struct utxocache_flush flush = {}; bpf_usdt_readarg(1, ctx, &flush.duration); bpf_usdt_readarg(2, ctx, &flush.mode); bpf_usdt_readarg(3, ctx, &flush.size); bpf_usdt_readarg(4, ctx, &flush.memory); bpf_usdt_readarg(5, ctx, &flush.for_prune); utxocache_flush.perf_submit(ctx, &flush, sizeof(flush)); return 0; } """ FLUSHMODE_NAME = { 0: "NONE", 1: "IF_NEEDED", 2: "PERIODIC", 3: "ALWAYS", } class UTXOCacheChange(ctypes.Structure): _fields_ = [ ("txid", ctypes.c_ubyte * 32), ("index", ctypes.c_uint32), ("height", ctypes.c_uint32), ("value", ctypes.c_ubyte * MAX_AMOUNT_LENGTH), ("is_coinbase", ctypes.c_bool), ] def __repr__(self): - return f"UTXOCacheChange(outpoint={bytes(self.txid[::-1]).hex()}:{self.index}, height={self.height}, value={self.value}, is_coinbase={self.is_coinbase})" + return ( + f"UTXOCacheChange(outpoint={bytes(self.txid[::-1]).hex()}:{self.index}," + f" height={self.height}, value={self.value}," + f" is_coinbase={self.is_coinbase})" + ) class UTXOCacheFlush(ctypes.Structure): _fields_ = [ ("duration", ctypes.c_int64), ("mode", ctypes.c_uint32), ("size", ctypes.c_uint64), ("memory", ctypes.c_uint64), ("for_prune", ctypes.c_bool), ] def __repr__(self): - return f"UTXOCacheFlush(duration={self.duration}, mode={FLUSHMODE_NAME[self.mode]}, size={self.size}, memory={self.memory}, for_prune={self.for_prune})" + return ( + f"UTXOCacheFlush(duration={self.duration}," + f" mode={FLUSHMODE_NAME[self.mode]}, size={self.size}," + f" memory={self.memory}, for_prune={self.for_prune})" + ) def c_string_to_str(c_string): - return bytes(c_string).split(b'\x00', 1)[0].decode('ascii') + return bytes(c_string).split(b"\x00", 1)[0].decode("ascii") class UTXOCacheTracepointTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = False self.num_nodes = 1 self.extra_args = [["-txindex"]] def skip_test_if_missing_module(self): self.skip_if_platform_not_linux() self.skip_if_no_bitcoind_tracepoints() self.skip_if_no_python_bcc() self.skip_if_no_bpf_permissions() def run_test(self): self.wallet = MiniWallet(self.nodes[0]) self.generate(self.wallet, 101) self.test_uncache() self.test_add_spent() self.test_flush() def test_uncache(self): - """ Tests the utxocache:uncache tracepoint API. + """Tests the utxocache:uncache tracepoint API. https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-utxocacheuncache """ # To trigger an UTXO uncache from the cache, we create an invalid transaction # spending a not-cached, but existing UTXO. During transaction validation, this # the UTXO is added to the utxo cache, but as the transaction is invalid, it's # uncached again. self.log.info("testing the utxocache:uncache tracepoint API") # Retrieve the txid for the UTXO created in the first block. This UTXO is not # in our UTXO cache. EARLY_BLOCK_HEIGHT = 1 block_1_hash = self.nodes[0].getblockhash(EARLY_BLOCK_HEIGHT) block_1 = self.nodes[0].getblock(block_1_hash) block_1_coinbase_txid = block_1["tx"][0] # Create a transaction and invalidate it by changing the txid of the previous # output to the coinbase txid of the block at height 1. - invalid_tx = self.wallet.create_self_transfer( - from_node=self.nodes[0])["tx"] + invalid_tx = self.wallet.create_self_transfer(from_node=self.nodes[0])["tx"] invalid_tx.vin[0].prevout.hash = int(block_1_coinbase_txid, 16) self.log.info("hooking into the utxocache:uncache tracepoint") ctx = USDT(pid=self.nodes[0].process.pid) - ctx.enable_probe(probe="utxocache:uncache", - fn_name="trace_utxocache_uncache") + ctx.enable_probe(probe="utxocache:uncache", fn_name="trace_utxocache_uncache") bpf = BPF(text=utxocache_changes_program, usdt_contexts=[ctx], debug=0) # The handle_* function is a ctypes callback function called from C. When # we assert in the handle_* function, the AssertError doesn't propagate # back to Python. The exception is ignored. We manually count and assert # that the handle_* functions succeeded. EXPECTED_HANDLE_UNCACHE_SUCCESS = 1 handle_uncache_succeeds = 0 def handle_utxocache_uncache(_, data, __): nonlocal handle_uncache_succeeds event = ctypes.cast(data, ctypes.POINTER(UTXOCacheChange)).contents self.log.info(f"handle_utxocache_uncache(): {event}") assert_equal(block_1_coinbase_txid, bytes(event.txid[::-1]).hex()) assert_equal(0, event.index) # prevout index assert_equal(EARLY_BLOCK_HEIGHT, event.height) assert_equal("50000000.00 XEC", c_string_to_str(event.value)) assert_equal(True, event.is_coinbase) handle_uncache_succeeds += 1 bpf["utxocache_uncache"].open_perf_buffer(handle_utxocache_uncache) self.log.info( - "testmempoolaccept the invalid transaction to trigger an UTXO-cache uncache") - result = self.nodes[0].testmempoolaccept( - [invalid_tx.serialize().hex()])[0] + "testmempoolaccept the invalid transaction to trigger an UTXO-cache uncache" + ) + result = self.nodes[0].testmempoolaccept([invalid_tx.serialize().hex()])[0] assert_equal(result["allowed"], False) bpf.perf_buffer_poll(timeout=100) bpf.cleanup() self.log.info( - f"check that we successfully traced {EXPECTED_HANDLE_UNCACHE_SUCCESS} uncaches") + "check that we successfully traced" + f" {EXPECTED_HANDLE_UNCACHE_SUCCESS} uncaches" + ) assert_equal(EXPECTED_HANDLE_UNCACHE_SUCCESS, handle_uncache_succeeds) def test_add_spent(self): - """ Tests the utxocache:add utxocache:spent tracepoint API - See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-utxocacheadd - and https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-utxocachespent + """Tests the utxocache:add utxocache:spent tracepoint API + See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-utxocacheadd + and https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-utxocachespent """ - self.log.info( - "test the utxocache:add and utxocache:spent tracepoint API") + self.log.info("test the utxocache:add and utxocache:spent tracepoint API") self.log.info("create an unconfirmed transaction") self.wallet.send_self_transfer(from_node=self.nodes[0]) # We mine a block to trace changes (add/spent) to the active in-memory cache # of the UTXO set (see CoinsTip() of CCoinsViewCache). However, in some cases # temporary clones of the active cache are made. For example, during mining with # the generate RPC call, the block is first tested in TestBlockValidity(). There, # a clone of the active cache is modified during a test ConnectBlock() call. # These are implementation details we don't want to test here. Thus, after # mining, we invalidate the block, start the tracing, and then trace the cache # changes to the active utxo cache. self.log.info("mine and invalidate a block that is later reconsidered") block_hash = self.generate(self.wallet, 1)[0] self.nodes[0].invalidateblock(block_hash) - self.log.info( - "hook into the utxocache:add and utxocache:spent tracepoints") + self.log.info("hook into the utxocache:add and utxocache:spent tracepoints") ctx = USDT(pid=self.nodes[0].process.pid) ctx.enable_probe(probe="utxocache:add", fn_name="trace_utxocache_add") - ctx.enable_probe(probe="utxocache:spent", - fn_name="trace_utxocache_spent") + ctx.enable_probe(probe="utxocache:spent", fn_name="trace_utxocache_spent") bpf = BPF(text=utxocache_changes_program, usdt_contexts=[ctx], debug=0) # The handle_* function is a ctypes callback function called from C. When # we assert in the handle_* function, the AssertError doesn't propagate # back to Python. The exception is ignored. We manually count and assert # that the handle_* functions succeeded. EXPECTED_HANDLE_ADD_SUCCESS = 2 EXPECTED_HANDLE_SPENT_SUCCESS = 1 handle_add_succeeds = 0 handle_spent_succeeds = 0 expected_utxocache_spents = [] expected_utxocache_adds = [] def handle_utxocache_add(_, data, __): nonlocal handle_add_succeeds event = ctypes.cast(data, ctypes.POINTER(UTXOCacheChange)).contents self.log.info(f"handle_utxocache_add(): {event}") add = expected_utxocache_adds.pop(0) assert_equal(add["txid"], bytes(event.txid[::-1]).hex()) assert_equal(add["index"], event.index) assert_equal(add["height"], event.height) assert_equal(add["value"], c_string_to_str(event.value)) assert_equal(add["is_coinbase"], event.is_coinbase) handle_add_succeeds += 1 def handle_utxocache_spent(_, data, __): nonlocal handle_spent_succeeds event = ctypes.cast(data, ctypes.POINTER(UTXOCacheChange)).contents self.log.info(f"handle_utxocache_spent(): {event}") spent = expected_utxocache_spents.pop(0) assert_equal(spent["txid"], bytes(event.txid[::-1]).hex()) assert_equal(spent["index"], event.index) assert_equal(spent["height"], event.height) assert_equal(spent["value"], c_string_to_str(event.value)) assert_equal(spent["is_coinbase"], event.is_coinbase) handle_spent_succeeds += 1 bpf["utxocache_add"].open_perf_buffer(handle_utxocache_add) bpf["utxocache_spent"].open_perf_buffer(handle_utxocache_spent) # We trigger a block re-connection. This causes changes (add/spent) # to the UTXO-cache which in turn triggers the tracepoints. self.log.info("reconsider the previously invalidated block") self.nodes[0].reconsiderblock(block_hash) block = self.nodes[0].getblock(block_hash, 2) - for (block_index, tx) in enumerate(block["tx"]): + for block_index, tx in enumerate(block["tx"]): for vin in tx["vin"]: if "coinbase" not in vin: - prevout_tx = self.nodes[0].getrawtransaction( - vin["txid"], True) + prevout_tx = self.nodes[0].getrawtransaction(vin["txid"], True) prevout_tx_block = self.nodes[0].getblockheader( - prevout_tx["blockhash"]) + prevout_tx["blockhash"] + ) spends_coinbase = "coinbase" in prevout_tx["vin"][0] - expected_utxocache_spents.append({ - "txid": vin["txid"], - "index": vin["vout"], - "height": prevout_tx_block["height"], - "value": f"{int(prevout_tx['vout'][vin['vout']]['value']):.2f} XEC", - "is_coinbase": spends_coinbase, - }) - for (i, vout) in enumerate(tx["vout"]): + expected_utxocache_spents.append( + { + "txid": vin["txid"], + "index": vin["vout"], + "height": prevout_tx_block["height"], + "value": ( + f"{int(prevout_tx['vout'][vin['vout']]['value']):.2f} XEC" + ), + "is_coinbase": spends_coinbase, + } + ) + for i, vout in enumerate(tx["vout"]): if vout["scriptPubKey"]["type"] != "nulldata": - expected_utxocache_adds.append({ - "txid": tx["txid"], - "index": i, - "height": block["height"], - "value": f"{int(vout['value']):.2f} XEC", - "is_coinbase": block_index == 0, - }) + expected_utxocache_adds.append( + { + "txid": tx["txid"], + "index": i, + "height": block["height"], + "value": f"{int(vout['value']):.2f} XEC", + "is_coinbase": block_index == 0, + } + ) assert_equal(EXPECTED_HANDLE_ADD_SUCCESS, len(expected_utxocache_adds)) - assert_equal(EXPECTED_HANDLE_SPENT_SUCCESS, - len(expected_utxocache_spents)) + assert_equal(EXPECTED_HANDLE_SPENT_SUCCESS, len(expected_utxocache_spents)) bpf.perf_buffer_poll(timeout=200) bpf.cleanup() self.log.info( - f"check that we successfully traced {EXPECTED_HANDLE_ADD_SUCCESS} adds and {EXPECTED_HANDLE_SPENT_SUCCESS} spent") + f"check that we successfully traced {EXPECTED_HANDLE_ADD_SUCCESS} adds and" + f" {EXPECTED_HANDLE_SPENT_SUCCESS} spent" + ) assert_equal(0, len(expected_utxocache_adds)) assert_equal(0, len(expected_utxocache_spents)) assert_equal(EXPECTED_HANDLE_ADD_SUCCESS, handle_add_succeeds) assert_equal(EXPECTED_HANDLE_SPENT_SUCCESS, handle_spent_succeeds) def test_flush(self): - """ Tests the utxocache:flush tracepoint API. - See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-utxocacheflush""" + """Tests the utxocache:flush tracepoint API. + See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-utxocacheflush + """ self.log.info("test the utxocache:flush tracepoint API") self.log.info("hook into the utxocache:flush tracepoint") ctx = USDT(pid=self.nodes[0].process.pid) - ctx.enable_probe(probe="utxocache:flush", - fn_name="trace_utxocache_flush") + ctx.enable_probe(probe="utxocache:flush", fn_name="trace_utxocache_flush") bpf = BPF(text=utxocache_flushes_program, usdt_contexts=[ctx], debug=0) # The handle_* function is a ctypes callback function called from C. When # we assert in the handle_* function, the AssertError doesn't propagate # back to Python. The exception is ignored. We manually count and assert # that the handle_* functions succeeded. EXPECTED_HANDLE_FLUSH_SUCCESS = 3 handle_flush_succeeds = 0 expected_flushes = [] def handle_utxocache_flush(_, data, __): nonlocal handle_flush_succeeds event = ctypes.cast(data, ctypes.POINTER(UTXOCacheFlush)).contents self.log.info(f"handle_utxocache_flush(): {event}") - expected_flushes.remove({ - "mode": FLUSHMODE_NAME[event.mode], - "for_prune": event.for_prune, - "size": event.size - }) + expected_flushes.remove( + { + "mode": FLUSHMODE_NAME[event.mode], + "for_prune": event.for_prune, + "size": event.size, + } + ) # sanity checks only assert event.memory > 0 assert event.duration > 0 handle_flush_succeeds += 1 bpf["utxocache_flush"].open_perf_buffer(handle_utxocache_flush) self.log.info("stop the node to flush the UTXO cache") # might need to be changed if the earlier tests are modified UTXOS_IN_CACHE = 2 # A node shutdown causes two flushes. One that flushes UTXOS_IN_CACHE # UTXOs and one that flushes 0 UTXOs. Normally the 0-UTXO-flush is the # second flush, however it can happen that the order changes. expected_flushes.append( - {"mode": "ALWAYS", "for_prune": False, "size": UTXOS_IN_CACHE}) - expected_flushes.append( - {"mode": "ALWAYS", "for_prune": False, "size": 0}) + {"mode": "ALWAYS", "for_prune": False, "size": UTXOS_IN_CACHE} + ) + expected_flushes.append({"mode": "ALWAYS", "for_prune": False, "size": 0}) self.stop_node(0) bpf.perf_buffer_poll(timeout=200) bpf.cleanup() self.log.info("check that we don't expect additional flushes") assert_equal(0, len(expected_flushes)) self.log.info("restart the node with -prune") self.start_node(0, ["-fastprune=1", "-prune=1"]) BLOCKS_TO_MINE = 350 self.log.info(f"mine {BLOCKS_TO_MINE} blocks to be able to prune") self.generate(self.wallet, BLOCKS_TO_MINE) self.log.info("test the utxocache:flush tracepoint API with pruning") self.log.info("hook into the utxocache:flush tracepoint") ctx = USDT(pid=self.nodes[0].process.pid) - ctx.enable_probe(probe="utxocache:flush", - fn_name="trace_utxocache_flush") + ctx.enable_probe(probe="utxocache:flush", fn_name="trace_utxocache_flush") bpf = BPF(text=utxocache_flushes_program, usdt_contexts=[ctx], debug=0) bpf["utxocache_flush"].open_perf_buffer(handle_utxocache_flush) self.log.info("prune blockchain to trigger a flush for pruning") expected_flushes.append({"mode": "NONE", "for_prune": True, "size": 0}) self.nodes[0].pruneblockchain(345) bpf.perf_buffer_poll(timeout=500) bpf.cleanup() self.log.info( - "check that we don't expect additional flushes and that the handle_* function succeeded") + "check that we don't expect additional flushes and that the handle_*" + " function succeeded" + ) assert_equal(0, len(expected_flushes)) assert_equal(EXPECTED_HANDLE_FLUSH_SUCCESS, handle_flush_succeeds) -if __name__ == '__main__': +if __name__ == "__main__": UTXOCacheTracepointTest().main() diff --git a/test/functional/interface_usdt_validation.py b/test/functional/interface_usdt_validation.py index b041ce406..75a132f9d 100755 --- a/test/functional/interface_usdt_validation.py +++ b/test/functional/interface_usdt_validation.py @@ -1,136 +1,136 @@ #!/usr/bin/env python3 # Copyright (c) 2022 The Bitcoin Core developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Tests the validation:* tracepoint API interface. See https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#context-validation """ import ctypes # Test will be skipped if we don't have bcc installed try: from bcc import BPF, USDT # type: ignore[import] except ImportError: pass from test_framework.address import ADDRESS_ECREG_UNSPENDABLE from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal validation_blockconnected_program = """ #include typedef signed long long i64; struct connected_block { char hash[32]; int height; i64 transactions; int inputs; i64 sigchk; u64 duration; }; BPF_PERF_OUTPUT(block_connected); int trace_block_connected(struct pt_regs *ctx) { struct connected_block block = {}; bpf_usdt_readarg_p(1, ctx, &block.hash, 32); bpf_usdt_readarg(2, ctx, &block.height); bpf_usdt_readarg(3, ctx, &block.transactions); bpf_usdt_readarg(4, ctx, &block.inputs); bpf_usdt_readarg(5, ctx, &block.sigchk); bpf_usdt_readarg(6, ctx, &block.duration); block_connected.perf_submit(ctx, &block, sizeof(block)); return 0; } """ class ValidationTracepointTest(BitcoinTestFramework): def set_test_params(self): self.num_nodes = 1 def skip_test_if_missing_module(self): self.skip_if_platform_not_linux() self.skip_if_no_bitcoind_tracepoints() self.skip_if_no_python_bcc() self.skip_if_no_bpf_permissions() def run_test(self): # Tests the validation:block_connected tracepoint by generating blocks # and comparing the values passed in the tracepoint arguments with the # blocks. # See # https://github.com/bitcoin/bitcoin/blob/master/doc/tracing.md#tracepoint-validationblock_connected class Block(ctypes.Structure): _fields_ = [ ("hash", ctypes.c_ubyte * 32), ("height", ctypes.c_int), ("transactions", ctypes.c_int64), ("inputs", ctypes.c_int), ("sigchk", ctypes.c_int64), ("duration", ctypes.c_uint64), ] def __repr__(self): return ( f"ConnectedBlock(hash={bytes(self.hash[::-1]).hex()} " f"height={self.height}, transactions={self.transactions}, " f"inputs={self.inputs}, sigchk={self.sigchk}, " f"duration={self.duration})" ) # The handle_* function is a ctypes callback function called from C. When # we assert in the handle_* function, the AssertError doesn't propagate # back to Python. The exception is ignored. We manually count and assert # that the handle_* functions succeeded. BLOCKS_EXPECTED = 2 blocks_checked = 0 expected_blocks = [] self.log.info("hook into the validation:block_connected tracepoint") ctx = USDT(pid=self.nodes[0].process.pid) - ctx.enable_probe(probe="validation:block_connected", - fn_name="trace_block_connected") - bpf = BPF(text=validation_blockconnected_program, - usdt_contexts=[ctx], debug=0) + ctx.enable_probe( + probe="validation:block_connected", fn_name="trace_block_connected" + ) + bpf = BPF(text=validation_blockconnected_program, usdt_contexts=[ctx], debug=0) def handle_blockconnected(_, data, __): nonlocal expected_blocks, blocks_checked event = ctypes.cast(data, ctypes.POINTER(Block)).contents self.log.info(f"handle_blockconnected(): {event}") block = expected_blocks.pop(0) assert_equal(block["hash"], bytes(event.hash[::-1]).hex()) assert_equal(block["height"], event.height) assert_equal(len(block["tx"]), event.transactions) assert_equal(len([tx["vin"] for tx in block["tx"]]), event.inputs) # no sigchk in coinbase tx assert_equal(0, event.sigchk) # only plausibility checks assert event.duration > 0 blocks_checked += 1 - bpf["block_connected"].open_perf_buffer( - handle_blockconnected) + bpf["block_connected"].open_perf_buffer(handle_blockconnected) self.log.info(f"mine {BLOCKS_EXPECTED} blocks") block_hashes = self.generatetoaddress( - self.nodes[0], BLOCKS_EXPECTED, ADDRESS_ECREG_UNSPENDABLE) + self.nodes[0], BLOCKS_EXPECTED, ADDRESS_ECREG_UNSPENDABLE + ) for block_hash in block_hashes: expected_blocks.append(self.nodes[0].getblock(block_hash, 2)) bpf.perf_buffer_poll(timeout=200) bpf.cleanup() self.log.info(f"check that we traced {BLOCKS_EXPECTED} blocks") assert_equal(BLOCKS_EXPECTED, blocks_checked) assert_equal(0, len(expected_blocks)) -if __name__ == '__main__': +if __name__ == "__main__": ValidationTracepointTest().main() diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py index befe838ac..aae415bb6 100755 --- a/test/functional/interface_zmq.py +++ b/test/functional/interface_zmq.py @@ -1,678 +1,698 @@ #!/usr/bin/env python3 # Copyright (c) 2015-2019 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 the ZMQ notification interface.""" import struct from io import BytesIO from time import sleep from test_framework.address import ADDRESS_ECREG_P2SH_OP_TRUE, ADDRESS_ECREG_UNSPENDABLE from test_framework.blocktools import ( create_block, create_coinbase, make_conform_to_ctor, ) from test_framework.messages import CTransaction, FromHex, hash256 from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, assert_raises_rpc_error # Test may be skipped and not have zmq installed try: import zmq except ImportError: pass def hash256_reversed(byte_str): return hash256(byte_str)[::-1] class ZMQSubscriber: def __init__(self, socket, topic): # no sequence number received yet self.sequence = None self.socket = socket self.topic = topic self.socket.setsockopt(zmq.SUBSCRIBE, self.topic) # Receive message from publisher and verify that topic and sequence match def _receive_from_publisher_and_check(self): topic, body, seq = self.socket.recv_multipart() # Topic should match the subscriber topic. assert_equal(topic, self.topic) # Sequence should be incremental. - received_seq = struct.unpack('C : Blockhash connected <32-byte hash>D : Blockhash disconnected <32-byte hash>R<8-byte LE uint> : Transactionhash removed from mempool for non-block inclusion reason <32-byte hash>A<8-byte LE uint> : Transactionhash added mempool """ self.log.info("Testing 'sequence' publisher") [seq] = self.setup_zmq_test([("sequence", "tcp://127.0.0.1:28333")]) self.disconnect_nodes(0, 1) # Mempool sequence number starts at 1 seq_num = 1 # Generate 1 block in nodes[0] and receive all notifications - dc_block = self.generatetoaddress(self.nodes[0], - 1, ADDRESS_ECREG_UNSPENDABLE, sync_fun=self.no_op)[0] + dc_block = self.generatetoaddress( + self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE, sync_fun=self.no_op + )[0] # Note: We are not notified of any block transactions, coinbase or # mined - assert_equal((self.nodes[0].getbestblockhash(), "C", None), - seq.receive_sequence()) + assert_equal( + (self.nodes[0].getbestblockhash(), "C", None), seq.receive_sequence() + ) # Generate 2 blocks in nodes[1] to a different address to ensure # a chain split self.generatetoaddress( - self.nodes[1], - 2, - ADDRESS_ECREG_P2SH_OP_TRUE, - sync_fun=self.no_op) + self.nodes[1], 2, ADDRESS_ECREG_P2SH_OP_TRUE, sync_fun=self.no_op + ) # nodes[0] will reorg chain after connecting back nodes[1] self.connect_nodes(0, 1) # Then we receive all block (dis)connect notifications for the # 2 block reorg assert_equal((dc_block, "D", None), seq.receive_sequence()) block_count = self.nodes[1].getblockcount() - assert_equal((self.nodes[1].getblockhash(block_count - 1), "C", None), - seq.receive_sequence()) - assert_equal((self.nodes[1].getblockhash(block_count), "C", None), - seq.receive_sequence()) + assert_equal( + (self.nodes[1].getblockhash(block_count - 1), "C", None), + seq.receive_sequence(), + ) + assert_equal( + (self.nodes[1].getblockhash(block_count), "C", None), seq.receive_sequence() + ) # Rest of test requires wallet functionality if self.is_wallet_compiled(): - (block_hash, txid_to_be_replaced, replacement_txid - ) = self.create_conflicting_tx() - self.log.info( - "Testing sequence notifications with mempool sequence values") + ( + block_hash, + txid_to_be_replaced, + replacement_txid, + ) = self.create_conflicting_tx() + self.log.info("Testing sequence notifications with mempool sequence values") # Should receive the initially broadcasted txid. - assert_equal((txid_to_be_replaced, "A", seq_num), - seq.receive_sequence()) + assert_equal((txid_to_be_replaced, "A", seq_num), seq.receive_sequence()) seq_num += 1 self.log.info("Testing a tx removal notification") # Next we receive a notification for the transaction removal - assert_equal((txid_to_be_replaced, "R", seq_num), - seq.receive_sequence()) + assert_equal((txid_to_be_replaced, "R", seq_num), seq.receive_sequence()) seq_num += 1 # Then we see the block notification assert_equal((block_hash, "C", None), seq.receive_sequence()) # There is no sequence notification for the transaction that was # never in node0's mempool, but it can be found in the block. assert replacement_txid in self.nodes[0].getblock(block_hash)["tx"] self.log.info("Wait for tx from second node") payment_txid = self.nodes[1].sendtoaddress( - address=self.nodes[0].getnewaddress(), amount=5_000_000) + address=self.nodes[0].getnewaddress(), amount=5_000_000 + ) self.sync_all() assert_equal((payment_txid, "A", seq_num), seq.receive_sequence()) seq_num += 1 # Doesn't get published when mined, make a block and tx to "flush" # the possibility though the mempool sequence number does go up by # the number of transactions removed from the mempool by the block # mining it. mempool_size = len(self.nodes[0].getrawmempool()) - c_block = self.generatetoaddress(self.nodes[0], - 1, ADDRESS_ECREG_UNSPENDABLE)[0] + c_block = self.generatetoaddress( + self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE + )[0] # Make sure the number of mined transactions matches the number of # txs out of mempool - mempool_size_delta = mempool_size - \ - len(self.nodes[0].getrawmempool()) - assert_equal(len(self.nodes[0].getblock(c_block)["tx"]) - 1, - mempool_size_delta) + mempool_size_delta = mempool_size - len(self.nodes[0].getrawmempool()) + assert_equal( + len(self.nodes[0].getblock(c_block)["tx"]) - 1, mempool_size_delta + ) seq_num += mempool_size_delta payment_txid_2 = self.nodes[1].sendtoaddress( - self.nodes[0].getnewaddress(), 1_000_000) + self.nodes[0].getnewaddress(), 1_000_000 + ) self.sync_all() assert_equal((c_block, "C", None), seq.receive_sequence()) - assert_equal((payment_txid_2, "A", seq_num), - seq.receive_sequence()) + assert_equal((payment_txid_2, "A", seq_num), seq.receive_sequence()) seq_num += 1 # Spot check getrawmempool results that they only show up when # asked for assert isinstance(self.nodes[0].getrawmempool(), list) - assert isinstance( - self.nodes[0].getrawmempool(mempool_sequence=False), - list) - assert "mempool_sequence" not in self.nodes[0].getrawmempool( - verbose=True) + assert isinstance(self.nodes[0].getrawmempool(mempool_sequence=False), list) + assert "mempool_sequence" not in self.nodes[0].getrawmempool(verbose=True) assert_raises_rpc_error( - -8, "Verbose results cannot contain mempool sequence values.", - self.nodes[0].getrawmempool, True, True) - assert_equal(self.nodes[0].getrawmempool( - mempool_sequence=True)["mempool_sequence"], - seq_num) + -8, + "Verbose results cannot contain mempool sequence values.", + self.nodes[0].getrawmempool, + True, + True, + ) + assert_equal( + self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"], + seq_num, + ) self.log.info("Testing reorg notifications") # Manually invalidate the last block to test mempool re-entry # N.B. This part could be made more lenient in exact ordering # since it greatly depends on inner-workings of blocks/mempool # during "deep" re-orgs. Probably should "re-construct" # blockchain/mempool state from notifications instead. block_count = self.nodes[0].getblockcount() best_hash = self.nodes[0].getbestblockhash() self.nodes[0].invalidateblock(best_hash) # Bit of room to make sure transaction things happened sleep(2) # Make sure getrawmempool mempool_sequence results aren't "queued" # but immediately reflective of the time they were gathered. - assert self.nodes[0].getrawmempool( - mempool_sequence=True)["mempool_sequence"] > seq_num + assert ( + self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"] + > seq_num + ) - assert_equal((payment_txid_2, "R", seq_num), - seq.receive_sequence()) + assert_equal((payment_txid_2, "R", seq_num), seq.receive_sequence()) seq_num += 1 assert_equal((best_hash, "D", None), seq.receive_sequence()) assert_equal((payment_txid, "A", seq_num), seq.receive_sequence()) seq_num += 1 # Other things may happen but aren't wallet-deterministic so we # don't test for them currently self.nodes[0].reconsiderblock(best_hash) self.generatetoaddress(self.nodes[1], 1, ADDRESS_ECREG_UNSPENDABLE) self.log.info("Evict mempool transaction by block conflict") orig_txid = self.nodes[0].sendtoaddress( - address=self.nodes[0].getnewaddress(), amount=1_000_000) + address=self.nodes[0].getnewaddress(), amount=1_000_000 + ) # More to be simply mined more_tx = [] for _ in range(5): - more_tx.append(self.nodes[0].sendtoaddress( - self.nodes[0].getnewaddress(), 100_000)) + more_tx.append( + self.nodes[0].sendtoaddress(self.nodes[0].getnewaddress(), 100_000) + ) raw_tx = self.nodes[0].getrawtransaction(orig_txid) block = create_block( int(self.nodes[0].getbestblockhash(), 16), - create_coinbase(self.nodes[0].getblockcount() + 1)) + create_coinbase(self.nodes[0].getblockcount() + 1), + ) tx = FromHex(CTransaction(), raw_tx) block.vtx.append(tx) for txid in more_tx: - tx = FromHex(CTransaction(), - self.nodes[0].getrawtransaction(txid)) + tx = FromHex(CTransaction(), self.nodes[0].getrawtransaction(txid)) block.vtx.append(tx) make_conform_to_ctor(block) block.hashMerkleRoot = block.calc_merkle_root() block.solve() - assert_equal(self.nodes[0].submitblock(block.serialize().hex()), - None) + assert_equal(self.nodes[0].submitblock(block.serialize().hex()), None) tip = self.nodes[0].getbestblockhash() assert_equal(int(tip, 16), block.sha256) orig_txid_2 = self.nodes[0].sendtoaddress( - address=self.nodes[0].getnewaddress(), amount=1_000_000) + address=self.nodes[0].getnewaddress(), amount=1_000_000 + ) # Flush old notifications until evicted tx original entry (hash_str, label, mempool_seq) = seq.receive_sequence() while hash_str != orig_txid: (hash_str, label, mempool_seq) = seq.receive_sequence() mempool_seq += 1 # Added original tx assert_equal(label, "A") # More transactions to be simply mined for i in range(len(more_tx)): - assert_equal((more_tx[i], "A", mempool_seq), - seq.receive_sequence()) + assert_equal((more_tx[i], "A", mempool_seq), seq.receive_sequence()) mempool_seq += 1 # Removed RBF tests mempool_seq += 1 assert_equal((tip, "C", None), seq.receive_sequence()) mempool_seq += len(more_tx) # Last tx - assert_equal((orig_txid_2, "A", mempool_seq), - seq.receive_sequence()) + assert_equal((orig_txid_2, "A", mempool_seq), seq.receive_sequence()) mempool_seq += 1 self.generatetoaddress(self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE) # want to make sure we didn't break "consensus" for other tests self.sync_all() def test_mempool_sync(self): """ Use sequence notification plus getrawmempool sequence results to "sync mempool" """ if not self.is_wallet_compiled(): self.log.info("Skipping mempool sync test") return self.log.info("Testing 'mempool sync' usage of sequence notifier") [seq] = self.setup_zmq_test([("sequence", "tcp://127.0.0.1:28333")]) # In-memory counter, should always start at 1 - next_mempool_seq = self.nodes[0].getrawmempool( - mempool_sequence=True)["mempool_sequence"] + next_mempool_seq = self.nodes[0].getrawmempool(mempool_sequence=True)[ + "mempool_sequence" + ] assert_equal(next_mempool_seq, 1) # Some transactions have been happening but we aren't consuming # zmq notifications yet or we lost a ZMQ message somehow and want # to start over txids = [] num_txs = 5 for _ in range(num_txs): - txids.append(self.nodes[1].sendtoaddress( - address=self.nodes[0].getnewaddress(), amount=1_000_000)) + txids.append( + self.nodes[1].sendtoaddress( + address=self.nodes[0].getnewaddress(), amount=1_000_000 + ) + ) self.sync_all() # 1) Consume backlog until we get a mempool sequence number (hash_str, label, zmq_mem_seq) = seq.receive_sequence() while zmq_mem_seq is None: (hash_str, label, zmq_mem_seq) = seq.receive_sequence() assert label == "A" assert hash_str is not None # 2) We need to "seed" our view of the mempool mempool_snapshot = self.nodes[0].getrawmempool(mempool_sequence=True) mempool_view = set(mempool_snapshot["txids"]) get_raw_seq = mempool_snapshot["mempool_sequence"] assert_equal(get_raw_seq, 6) # Snapshot may be too old compared to zmq message we read off latest while zmq_mem_seq >= get_raw_seq: sleep(2) - mempool_snapshot = self.nodes[0].getrawmempool( - mempool_sequence=True) + mempool_snapshot = self.nodes[0].getrawmempool(mempool_sequence=True) mempool_view = set(mempool_snapshot["txids"]) get_raw_seq = mempool_snapshot["mempool_sequence"] # Things continue to happen in the "interim" while waiting for # snapshot results for _ in range(num_txs): - txids.append(self.nodes[0].sendtoaddress( - address=self.nodes[0].getnewaddress(), amount=1_000_000)) + txids.append( + self.nodes[0].sendtoaddress( + address=self.nodes[0].getnewaddress(), amount=1_000_000 + ) + ) self.sync_all() self.create_conflicting_tx() self.generatetoaddress(self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE) final_txid = self.nodes[0].sendtoaddress( - address=self.nodes[0].getnewaddress(), amount=100_000) + address=self.nodes[0].getnewaddress(), amount=100_000 + ) # 3) Consume ZMQ backlog until we get to "now" for the mempool snapshot while True: if zmq_mem_seq == get_raw_seq - 1: break (hash_str, label, mempool_sequence) = seq.receive_sequence() if mempool_sequence is not None: zmq_mem_seq = mempool_sequence if zmq_mem_seq > get_raw_seq: raise Exception( - f"We somehow jumped mempool sequence numbers! " + "We somehow jumped mempool sequence numbers! " f"zmq_mem_seq: {zmq_mem_seq} > " - f"get_raw_seq: {get_raw_seq}") + f"get_raw_seq: {get_raw_seq}" + ) # 4) Moving forward, we apply the delta to our local view # remaining txs + conflict (A, R, C) + 1 block connect + 1 final tx expected_sequence = get_raw_seq for _ in range(num_txs + 3 + 1 + 1): (hash_str, label, mempool_sequence) = seq.receive_sequence() if label == "A": assert hash_str not in mempool_view mempool_view.add(hash_str) expected_sequence = mempool_sequence + 1 elif label == "R": assert hash_str in mempool_view mempool_view.remove(hash_str) expected_sequence = mempool_sequence + 1 elif label == "C": # (Attempt to) remove all txids from known block connects block_txids = self.nodes[0].getblock(hash_str)["tx"][1:] for txid in block_txids: if txid in mempool_view: expected_sequence += 1 mempool_view.remove(txid) elif label == "D": # Not useful for mempool tracking per se continue else: raise Exception("Unexpected ZMQ sequence label!") assert_equal(self.nodes[0].getrawmempool(), [final_txid]) assert_equal( - self.nodes[0].getrawmempool( - mempool_sequence=True)["mempool_sequence"], - expected_sequence) + self.nodes[0].getrawmempool(mempool_sequence=True)["mempool_sequence"], + expected_sequence, + ) # 5) If you miss a zmq/mempool sequence number, go back to step (2) self.generatetoaddress(self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE) def test_multiple_interfaces(self): # Set up two subscribers with different addresses # (note that after the reorg test, syncing would fail due to different # chain lengths on node0 and node1; for this test we only need node0, so # we can disable syncing blocks on the setup) - subscribers = self.setup_zmq_test([ - ("hashblock", "tcp://127.0.0.1:28334"), - ("hashblock", "tcp://127.0.0.1:28335"), - ], sync_blocks=False) + subscribers = self.setup_zmq_test( + [ + ("hashblock", "tcp://127.0.0.1:28334"), + ("hashblock", "tcp://127.0.0.1:28335"), + ], + sync_blocks=False, + ) # Generate 1 block in nodes[0] and receive all notifications self.generatetoaddress( - self.nodes[0], - 1, - ADDRESS_ECREG_UNSPENDABLE, - sync_fun=self.no_op) + self.nodes[0], 1, ADDRESS_ECREG_UNSPENDABLE, sync_fun=self.no_op + ) # Should receive the same block hash on both subscribers - assert_equal(self.nodes[0].getbestblockhash(), - subscribers[0].receive().hex()) - assert_equal(self.nodes[0].getbestblockhash(), - subscribers[1].receive().hex()) + assert_equal(self.nodes[0].getbestblockhash(), subscribers[0].receive().hex()) + assert_equal(self.nodes[0].getbestblockhash(), subscribers[1].receive().hex()) -if __name__ == '__main__': +if __name__ == "__main__": ZMQTest().main()