Connecting to Bitcoin Core's ZeroMQ Interface Over Tor

Under Bitcoin Core’s listed interfaces, I see the ZeroMQ interface with an .onion address (no .local option). I’m having trouble connecting to it. I know this is a long shot, but has anyone successfully connected to this interface?

I have not been successful with Python’s PyZMQ library. I did find an example in Bitcoin Core’s repo: bitcoin/contrib/zmq/zmq_sub.py at master · bitcoin/bitcoin · GitHub, but I could not get it to work.

I have the Tor daemon running and can access .onion sites using the requests library, so I know Tor is working. A simple example that returns the first 500 characters of an .onion URL:

import requests

onion_url = "http://some-onion-address.onion"
proxies = {
    "http": "socks5h://127.0.0.1:9050",
    "https": "socks5h://127.0.0.1:9050",
}

try:
    response = requests.get(onion_url, proxies=proxies, timeout=10)
    print("Status Code:", response.status_code)
    print("Response Text:", response.text[:500])
except requests.exceptions.RequestException as e:
    print("Failed to connect to .onion site:", e)

Interestingly, if I set onion_url to http://my-bitcoin-core-zeromq-address.onion:28333, I get a ZMQ message (or part of one) from my node embedded in the exception message! Obviously, the error is because I’m trying to use the requests library (using the HTTP protocol) on a socket publishing data using the ZMQ protocol, but you can see the ZMQ topic “hashtx” appears at byte 12:

Failed to connect to .onion site: (‘Connection aborted.’, BadStatusLine(“ÿ\x00\x00\x00\x00\x00\x00\x00\x01\x7f\x07\x01hashtx!\x01\x06\x0c...full tx hash scrubbed”))

Any ideas on getting PyZMQ to work? Here’s the minimal example I was expecting to work based on what I’ve read:

ZMQ Over Tor Attempt
import zmq
import socket
import socks
import time

zmq_addr = 'tcp://my-bitcoin-core-zeromq-address.onion:28333'
timeout = 30

socks.set_default_proxy(socks.SOCKS5, '127.0.0.1', 9050)
socket.socket = socks.socksocket

zmq_context = zmq.Context()
zmq_socket = zmq_context.socket(zmq.SUB)
zmq_socket.setsockopt(zmq.RCVHWM, 0)
for topic in ['hashblock', 'hashtx', 'rawblock', 'rawtx', 'sequence']:
    zmq_socket.setsockopt_string(zmq.SUBSCRIBE, topic)
zmq_socket.connect(zmq_addr)

start_time = time.time()
while (start_time + timeout > time.time()):
    try:
        message = zmq_socket.recv_multipart(flags=zmq.NOBLOCK)
        print(f'Received message: {message}')
    except zmq.Again:
        continue  # no message
    except Exception as e:
        print(f'Error: {e}', exc_info=True)

zmq_socket.close()
zmq_context.destroy()

Well, I worked on this for two days, finally decided to ask the internet, and then stumbled across the answer 20 min later. Oh well, I’ll take it!

The answer is that I needed to set this option:

zmq_socket.setsockopt_string(zmq.SOCKS_PROXY, '127.0.0.1:9050')

(And the socket to socksocket monkey patching can be removed.)