diff --git a/doc/release-notes.md b/doc/release-notes.md --- a/doc/release-notes.md +++ b/doc/release-notes.md @@ -9,3 +9,5 @@ is similar to `listunspent` but does not use a wallet, meaning that the wallet can be disabled at compile or run time. This call is experimental, as such, is subject to changes or removal in future releases. + - The new RPC `getzmqnotifications` returns information about active ZMQ + notifications. diff --git a/src/Makefile.am b/src/Makefile.am --- a/src/Makefile.am +++ b/src/Makefile.am @@ -232,7 +232,8 @@ zmq/zmqabstractnotifier.h \ zmq/zmqconfig.h\ zmq/zmqnotificationinterface.h \ - zmq/zmqpublishnotifier.h + zmq/zmqpublishnotifier.h \ + zmq/zmqrpc.h obj/build.h: FORCE @@ -302,7 +303,8 @@ libbitcoin_zmq_a_SOURCES = \ zmq/zmqabstractnotifier.cpp \ zmq/zmqnotificationinterface.cpp \ - zmq/zmqpublishnotifier.cpp + zmq/zmqpublishnotifier.cpp \ + zmq/zmqrpc.cpp endif diff --git a/src/init.cpp b/src/init.cpp --- a/src/init.cpp +++ b/src/init.cpp @@ -56,6 +56,7 @@ #if ENABLE_ZMQ #include +#include #endif #ifndef WIN32 @@ -96,10 +97,6 @@ const WalletInitInterface &g_wallet_init_interface = DummyWalletInit(); #endif -#if ENABLE_ZMQ -static CZMQNotificationInterface *pzmqNotificationInterface = nullptr; -#endif - #ifdef WIN32 // Win32 LevelDB doesn't use filedescriptors, and the ones used for accessing // block files don't count towards the fd_set size limit anyway. @@ -271,10 +268,10 @@ g_wallet_init_interface.Stop(); #if ENABLE_ZMQ - if (pzmqNotificationInterface) { - UnregisterValidationInterface(pzmqNotificationInterface); - delete pzmqNotificationInterface; - pzmqNotificationInterface = nullptr; + if (g_zmq_notification_interface) { + UnregisterValidationInterface(g_zmq_notification_interface); + delete g_zmq_notification_interface; + g_zmq_notification_interface = nullptr; } #endif @@ -1916,6 +1913,9 @@ */ RegisterAllRPCCommands(config, rpcServer, tableRPC); g_wallet_init_interface.RegisterRPC(tableRPC); +#if ENABLE_ZMQ + RegisterZMQRPCCommands(tableRPC); +#endif /** * Start the RPC server. It will be started in "warmup" mode and not @@ -2063,10 +2063,10 @@ } #if ENABLE_ZMQ - pzmqNotificationInterface = CZMQNotificationInterface::Create(); + g_zmq_notification_interface = CZMQNotificationInterface::Create(); - if (pzmqNotificationInterface) { - RegisterValidationInterface(pzmqNotificationInterface); + if (g_zmq_notification_interface) { + RegisterValidationInterface(g_zmq_notification_interface); } #endif // unlimited unless -maxuploadtarget is set diff --git a/src/zmq/CMakeLists.txt b/src/zmq/CMakeLists.txt --- a/src/zmq/CMakeLists.txt +++ b/src/zmq/CMakeLists.txt @@ -6,6 +6,7 @@ zmqabstractnotifier.cpp zmqnotificationinterface.cpp zmqpublishnotifier.cpp + zmqrpc.cpp ) find_package(ZeroMQ REQUIRED) diff --git a/src/zmq/zmqnotificationinterface.h b/src/zmq/zmqnotificationinterface.h --- a/src/zmq/zmqnotificationinterface.h +++ b/src/zmq/zmqnotificationinterface.h @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2016 The Bitcoin Core developers +// Copyright (c) 2015-2018 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -17,6 +17,8 @@ public: virtual ~CZMQNotificationInterface(); + std::list GetActiveNotifiers() const; + static CZMQNotificationInterface *Create(); protected: @@ -42,4 +44,6 @@ std::list notifiers; }; +extern CZMQNotificationInterface *g_zmq_notification_interface; + #endif // BITCOIN_ZMQ_ZMQNOTIFICATIONINTERFACE_H diff --git a/src/zmq/zmqnotificationinterface.cpp b/src/zmq/zmqnotificationinterface.cpp --- a/src/zmq/zmqnotificationinterface.cpp +++ b/src/zmq/zmqnotificationinterface.cpp @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2016 The Bitcoin Core developers +// Copyright (c) 2015-2018 The Bitcoin Core developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. @@ -26,6 +26,15 @@ } } +std::list +CZMQNotificationInterface::GetActiveNotifiers() const { + std::list result; + for (const auto *n : notifiers) { + result.push_back(n); + } + return result; +} + CZMQNotificationInterface *CZMQNotificationInterface::Create() { CZMQNotificationInterface *notificationInterface = nullptr; std::map factories; @@ -168,3 +177,5 @@ TransactionAddedToMempool(ptx); } } + +CZMQNotificationInterface *g_zmq_notification_interface = nullptr; diff --git a/src/zmq/zmqrpc.h b/src/zmq/zmqrpc.h new file mode 100644 --- /dev/null +++ b/src/zmq/zmqrpc.h @@ -0,0 +1,12 @@ +// Copyright (c) 2018 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_ZMQ_ZMQRPC_H +#define BITCOIN_ZMQ_ZMQRPC_H + +class CRPCTable; + +void RegisterZMQRPCCommands(CRPCTable &t); + +#endif // BITCOIN_ZMQ_ZMQRPC_H diff --git a/src/zmq/zmqrpc.cpp b/src/zmq/zmqrpc.cpp new file mode 100644 --- /dev/null +++ b/src/zmq/zmqrpc.cpp @@ -0,0 +1,62 @@ +// Copyright (c) 2018 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include + +#include + +namespace { + +UniValue getzmqnotifications(const Config &config, + const JSONRPCRequest &request) { + if (request.fHelp || request.params.size() != 0) { + throw std::runtime_error( + "getzmqnotifications\n" + "\nReturns information about the active ZeroMQ notifications.\n" + "\nResult:\n" + "[\n" + " { (json object)\n" + " \"type\": \"pubhashtx\", (string) Type of notification\n" + " \"address\": \"...\" (string) Address of the publisher\n" + " },\n" + " ...\n" + "]\n" + "\nExamples:\n" + + HelpExampleCli("getzmqnotifications", "") + + HelpExampleRpc("getzmqnotifications", "")); + } + + UniValue result(UniValue::VARR); + if (g_zmq_notification_interface != nullptr) { + for (const auto *n : + g_zmq_notification_interface->GetActiveNotifiers()) { + UniValue obj(UniValue::VOBJ); + obj.pushKV("type", n->GetType()); + obj.pushKV("address", n->GetAddress()); + result.push_back(obj); + } + } + + return result; +} + +// clang-format off +static const ContextFreeRPCCommand commands[] = { + // category name actor (function) argNames + // ----------------- ------------------------ ----------------------- ---------- + { "zmq", "getzmqnotifications", getzmqnotifications, {} }, +}; +// clang-format on + +} // anonymous namespace + +void RegisterZMQRPCCommands(CRPCTable &t) { + for (const auto &c : commands) { + t.appendCommand(c.name, &c); + } +} diff --git a/test/functional/interface_zmq.py b/test/functional/interface_zmq.py --- a/test/functional/interface_zmq.py +++ b/test/functional/interface_zmq.py @@ -3,11 +3,14 @@ # 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 configparser import struct from io import BytesIO -from test_framework.test_framework import BitcoinTestFramework, SkipTest +from test_framework.test_framework import ( + BitcoinTestFramework, + skip_if_no_bitcoind_zmq, + skip_if_no_py3_zmq, +) from test_framework.messages import CTransaction from test_framework.util import ( assert_equal, @@ -39,18 +42,9 @@ self.num_nodes = 2 def setup_nodes(self): - # Try to import python3-zmq. Skip this test if the import fails. - try: - import zmq - except ImportError: - raise SkipTest("python3-zmq module not available.") - - # Check that bitcoin has been built with ZMQ enabled. - config = configparser.ConfigParser() - config.read_file(open(self.options.configfile, encoding='utf-8')) - - if not config["components"].getboolean("ENABLE_ZMQ"): - raise SkipTest("bitcoind has not been built with zmq enabled.") + skip_if_no_py3_zmq() + skip_if_no_bitcoind_zmq(self) + import zmq # Initialize ZMQ context and socket. # All messages are received in the same socket which means that this diff --git a/test/functional/rpc_zmq.py b/test/functional/rpc_zmq.py new file mode 100755 --- /dev/null +++ b/test/functional/rpc_zmq.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +# Copyright (c) 2018 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 for the ZMQ RPC methods.""" + +from test_framework.test_framework import ( + BitcoinTestFramework, + skip_if_no_bitcoind_zmq, + skip_if_no_py3_zmq, +) +from test_framework.util import assert_equal + + +class RPCZMQTest(BitcoinTestFramework): + + address = "tcp://127.0.0.1:28332" + + def set_test_params(self): + self.num_nodes = 1 + self.setup_clean_chain = True + + def run_test(self): + skip_if_no_py3_zmq() + skip_if_no_bitcoind_zmq(self) + self._test_getzmqnotifications() + + def _test_getzmqnotifications(self): + self.restart_node(0, extra_args=[]) + assert_equal(self.nodes[0].getzmqnotifications(), []) + + self.restart_node( + 0, extra_args=["-zmqpubhashtx={}".format(self.address)]) + assert_equal(self.nodes[0].getzmqnotifications(), [ + {"type": "pubhashtx", "address": self.address}, + ]) + + +if __name__ == '__main__': + RPCZMQTest().main() diff --git a/test/functional/test_framework/test_framework.py b/test/functional/test_framework/test_framework.py --- a/test/functional/test_framework/test_framework.py +++ b/test/functional/test_framework/test_framework.py @@ -475,3 +475,20 @@ def __init__(self, message): self.message = message + + +def skip_if_no_py3_zmq(): + """Attempt to import the zmq package and skip the test if the import fails.""" + try: + import zmq # noqa + except ImportError: + raise SkipTest("python3-zmq module not available.") + + +def skip_if_no_bitcoind_zmq(test_instance): + """Skip the running test if bitcoind has not been compiled with zmq support.""" + config = configparser.ConfigParser() + config.read_file(open(test_instance.options.configfile, encoding='utf-8')) + + if not config["components"].getboolean("ENABLE_ZMQ"): + raise SkipTest("bitcoind has not been built with zmq enabled.") diff --git a/test/lint/check-rpc-mappings.py b/test/lint/check-rpc-mappings.py --- a/test/lint/check-rpc-mappings.py +++ b/test/lint/check-rpc-mappings.py @@ -14,6 +14,7 @@ SOURCE_PATTERNS = [ "src/rpc/*.cpp", "src/wallet/rpc*.cpp", + "src/zmq/zmqrpc.cpp", ] # Source file (relative to root) containing conversion mapping SOURCE_CLIENT = 'src/rpc/client.cpp'