diff --git a/docs/onion-message-channels.md b/docs/onion-message-channels.md index 5926f43..e883de5 100644 --- a/docs/onion-message-channels.md +++ b/docs/onion-message-channels.md @@ -17,6 +17,8 @@ introduce any new requirements to your Joinmarket installation, technically, bec to run such onion services, and connecting to IRC used a SOCKS5 proxy (used by almost all users) over Tor to a remote onion service. +(Note however that taker bots will *not* be required to serve onions; they will only make outbound SOCKS connections, as they currently do on IRC). + The purpose of this new type of message channel is as follows: * less reliance on any service external to Joinmarket @@ -25,7 +27,7 @@ albeit it was and remains E2E encrypted data, in either case) * the above can lead to better scalability at large numbers * a substantial increase in the speed of transaction negotiation; this is mostly related to the throttling of high bursts of traffic on IRC -The configuration for a user is simple; in their `joinmarket.cfg` they will get a messaging section like this, if they start from scratch: +The configuration for a user is simple; in their `joinmarket.cfg` they will get a new `[MESSAGING]` section like this, if they start from scratch: ``` [MESSAGING:onion] @@ -64,23 +66,26 @@ hidden_service_dir = # This is a comma separated list (comma can be omitted if only one item). # Each item has format host:port ; both are required, though port will # be 80 if created in this code. -directory_nodes = rr6f6qtleiiwic45bby4zwmiwjrj3jsbmcvutwpqxjziaydjydkk5iad.onion:80 +# for MAINNET: +directory_nodes = 3kxw6lf5vf6y26emzwgibzhrzhmhqiw6ekrek3nqfjjmhwznb2moonad.onion,qqd22cwgygaxcy6vdw6mzwkyaxg5urb4ptbc5d74nrj25phspajxjbqd.onion + +# for SIGNET (testing network): +# directory_nodes = rr6f6qtleiiwic45bby4zwmiwjrj3jsbmcvutwpqxjziaydjydkk5iad.onion:80,k74oyetjqgcamsyhlym2vgbjtvhcrbxr4iowd4nv4zk5sehw4v665jad.onion:80 # This setting is ONLY for developer regtest setups, # running multiple bots at once. Don't alter it otherwise regtest_count = 0,0 ``` -All of these can be left as default for most users, except the field `directory_nodes`. +All of these can be left as default for most users - but most importantly, pay attention to: -The list of **directory nodes** (the one shown here is one being run on signet, right now), which will -be comma separated if multiple directory nodes are configured (we expect there will be 2 or 3 as a normal situation). -The `onion_serving_port` is on which port on the local machine the onion service is served; you won't usually need to use it, but it mustn't conflict with some other usage (so if you have something running on port 8080, change it). +* The list of `directory_nodes`, which will be comma separated if multiple directory nodes are configured (we expect there will be 2 or 3 as a normal situation). Make sure to choose the ones for your network (mainnet by default, or signet or otherwise); if it's wrong your bot will just get auto-disconnected. +* The `onion_serving_port` is the port on the local machine on which the onion service is served; you won't usually need to use it, but it mustn't conflict with some other usage (so if you have something running on port 8080, change it). The `type` field must always be `onion` in this case, and distinguishes it from IRC message channels and others. ### Can/should I still run IRC message channels? -In short, yes. +In short, yes, at least for now, though you are free to disable any message channel you like. ### Do I need to configure Tor, and if so, how? @@ -95,7 +100,7 @@ This onion service will be ephemeral, that is, it will have a different onion ad you restart. This should work automatically, using your existing Tor daemon (here, we are using the same code as we use when running the `receive-payjoin` script, essentially). -#### Running/testing as other bots (taker) +#### Running/testing as other bots (taker, ob-watcher) A taker will not attempt to serve an onion; it will only use outbound connections, first to directory nodes and then, as according to need, to individual makers, also. @@ -132,9 +137,7 @@ and pay attention to the settings in `regtest_joinmarket.cfg`.) There is no separate/special configuration for signet other than the configuration that is already needed for running Joinmarket against a signet backend (so e.g. RPC port of 38332). -Add the `[MESSAGING:onion]` message channel section to your `joinmarket.cfg`, as listed above, including the -signet directory node listed above (rr6f6qtleiiwic45bby4zwmiwjrj3jsbmcvutwpqxjziaydjydkk5iad.onion:80), and, -for the simplest test, remove the other `[MESSAGING:*]` sections that you have. +You can just uncomment the `directory_nodes` entry listed as SIGNET, and comment out the one for MAINNET. Then just make sure your bot has some signet coins and try running as maker or taker or both. @@ -148,12 +151,17 @@ who would like to help by running a directory node. You can ignore it if that do This requires a long running bot. It should be on a server you can keep running permanently, so perhaps a VPS, but in any case, very high uptime. For reliability it also makes sense to configure to run as a systemd service. -A note: the most natural way to run the directory is as a Joinmarket *maker* bot, i.e. run `yg-privacyenhanced.py`, with configuration as described below. For now it will actually offer to do coinjoins - we will want to fix this in future so no coins are needed (but it can just be a trivial size). +The currently suggested way to run a directory node is to use the script found [here](https://github.com/JoinMarket-Org/custom-scripts/blob/0eda6154265e012b907c43e2ecdacb895aa9e3ab/start-dn.py); you can place it in your `joinmarket-clientserver/scripts` directory and run it *without* arguments, but with one option flag: `--datadir=/your/chosen/datadir` (as you'll see below). + +This slightly unobvious approach is based on the following ideas: we run a Joinmarket script, with a Joinmarket python virtualenv, so that we are able to parse messages; this means that the directory node *can* be a bot, e.g. a maker bot, but need not be - and here it is basically a "crippled" maker bot that cannot do anything. This 'crippling' is actually very useful because (a) we use the `no-blockchain` argument (it is forced in-code; you don't need to set it) so we don't need a running Bitcoin node (of whatever flavour), and (b) we don't need a wallet either. #### Joinmarket-specific configuration -Add `hidden_service_dir` to your `[MESSAGING:onion]` with a directory accessible to your user. You may want to lock this down -a bit! +Add a non-empty `hidden_service_dir` entry to your `[MESSAGING:onion]` with a directory accessible to your user. You may want to lock this down +a bit, but be careful changing permissions from what is created by the script, because Tor is very finicky about this. + +The hostname for your onion service will not change and will be stored permanently in that directory. + The point to understand is: Joinmarket's `jmbase.JMHiddenService` will, if configured with a non-empty `hidden_service_dir` field, actually start an *independent* instance of Tor specifically for serving this, under the current user. (our Tor interface library `txtorcon` needs read access to the Tor HS dir, so it's troublesome to do this another way). @@ -162,58 +170,40 @@ field, actually start an *independent* instance of Tor specifically for serving Answer: **you must only enter your own node in this list!**. This way your bot will recognize that it is a directory node and it avoids weird edge case behaviour (so don't add *other* known directory nodes; you won't be talking to them). +A natural retort is: but I don't know my own node's onion service hostname before I start it the first time. Indeed. So, just run it once with the default `directory_nodes` entries, then note down the new onion service hostname you created, and insert that as the only entry in the list. -#### Suggested setup of a service: -You will need two components: bitcoind, and Joinmarket itself, which you can run as a yg. -Since this task is going to be attempted by someone with significant technical knowledge, -only an outline is provided here; several details will need to be filled in. -Here is a sketch of how the systemd service files can be set up for signet: +#### Suggested setup of a systemd service: -If someone wants to put together a docker setup of this for a more "one-click install", that would be great. - -1. bitcoin-signet.service +The most basic bare-bones service seems to work fine here: ``` [Unit] -Description=bitcoind signet +Description=My JM signet directory node +Requires=network-online.target After=network-online.target -Wants=network-online.target [Service] Type=simple -ExecStart=/usr/local/bin/bitcoind -signet +ExecStart=/bin/bash -c 'cd /path/to/joinmarket-clientserver && source jmvenv/bin/activate && cd scripts && python start-dn.py --datadir=/path/to/chosen/datadir' User=user [Install] WantedBy=multi-user.target ``` -This is deliberately a super-basic setup (see above). Don't forget to setup your `bitcoin.conf` as usual, -for the bitcoin user, and make it match (specifically in terms of RPC) what you set up for Joinmarket below. +... however, you need to kind of 'bootstrap' it the first time. For example: +* run once with systemctl start -2. +* look at log with `journalctl`, service fails due to default `joinmarket.cfg` and quit. +* go to that cfg file. Remove the IRC settings, they serve no purpose here. Change the `hidden_service_dir` to `/yourlocation/hidserv` (the actual directory need not exist, it's better if it doesn't, this first time). Edit the `network` field in `BLOCKCHAIN` to whatever network (mainnet, signet) you intend to support - it can be only one for one directory node, for now. -``` -[Unit] -Description=joinmarket directory node on signet -Requires=bitcoin-signet.service -After=bitcoin-signet.service +* `systemctl start` again, now note the onion hostname created from the log or the directory -[Service] -Type=simple -ExecStart=/bin/bash -c 'cd /path/to/joinmarket-clientserver && source jmvenv/bin/activate && cd scripts && echo -n "password" | python yg-privacyenhanced.py --wallet-password-stdin --datadir=/custom/joinmarket-datadir some-signet-wallet.jmdat' -User=user - -[Install] -WantedBy=multi-user.target -``` +* set that hostname in `directory_nodes` in `joinmarket.cfg` -To state the obvious, the idea here is that this second service will run the JM directory node and have a dependency on the previous one, -to ensure they start up in the correct order. - -Re: password echo, obviously this kind of password entry is bad; -for now we needn't worry as these nodes don't need to carry significant coins (and it's much better they don't!). +* now the service should start correctly TODO: add some material on network hardening/firewalls here, I guess. + diff --git a/jmclient/jmclient/__init__.py b/jmclient/jmclient/__init__.py index ca91ace..086509c 100644 --- a/jmclient/jmclient/__init__.py +++ b/jmclient/jmclient/__init__.py @@ -32,7 +32,7 @@ from .blockchaininterface import (BlockchainInterface, from .snicker_receiver import SNICKERError, SNICKERReceiver from .client_protocol import (JMTakerClientProtocol, JMClientProtocolFactory, start_reactor, SNICKERClientProtocolFactory, - BIP78ClientProtocolFactory, + BIP78ClientProtocolFactory, JMMakerClientProtocol, get_daemon_serving_params) from .podle import (set_commitment_file, get_commitment_file, add_external_commitments, diff --git a/jmclient/jmclient/configure.py b/jmclient/jmclient/configure.py index ea04d0d..08b0e71 100644 --- a/jmclient/jmclient/configure.py +++ b/jmclient/jmclient/configure.py @@ -173,7 +173,11 @@ hidden_service_dir = # This is a comma separated list (comma can be omitted if only one item). # Each item has format host:port ; both are required, though port will # be 80 if created in this code. -directory_nodes = rr6f6qtleiiwic45bby4zwmiwjrj3jsbmcvutwpqxjziaydjydkk5iad.onion:80,k74oyetjqgcamsyhlym2vgbjtvhcrbxr4iowd4nv4zk5sehw4v665jad.onion:80 +# for MAINNET: +directory_nodes = 3kxw6lf5vf6y26emzwgibzhrzhmhqiw6ekrek3nqfjjmhwznb2moonad.onion:80,qqd22cwgygaxcy6vdw6mzwkyaxg5urb4ptbc5d74nrj25phspajxjbqd.onion:80 + +# for SIGNET (testing network): +# directory_nodes = rr6f6qtleiiwic45bby4zwmiwjrj3jsbmcvutwpqxjziaydjydkk5iad.onion:80,k74oyetjqgcamsyhlym2vgbjtvhcrbxr4iowd4nv4zk5sehw4v665jad.onion:80 # This setting is ONLY for developer regtest setups, # running multiple bots at once. Don't alter it otherwise @@ -211,7 +215,7 @@ socks5_host = localhost host = ilitafrzzgxymv6umx2ux7kbz3imyeko6cnqkvy4nisjjj4qpqkrptid.onion socks5_port = 9050 -## IRC SERVER 3) (backup) hackint IRC (Tor, IP) +## IRC SERVER 3: (backup) hackint IRC (Tor, IP) ################################################################################ #[MESSAGING:server3] # channel = joinmarket-pit diff --git a/jmdaemon/jmdaemon/onionmc.py b/jmdaemon/jmdaemon/onionmc.py index 3d8bf8f..400053f 100644 --- a/jmdaemon/jmdaemon/onionmc.py +++ b/jmdaemon/jmdaemon/onionmc.py @@ -9,13 +9,17 @@ from twisted.protocols import basic from twisted.application.internet import ClientService from twisted.internet.endpoints import TCP4ClientEndpoint from twisted.internet.address import IPv4Address, IPv6Address -from txtorcon.socks import TorSocksEndpoint +from txtorcon.socks import TorSocksEndpoint, HostUnreachableError log = get_log() NOT_SERVING_ONION_HOSTNAME = "NOT-SERVING-ONION" +# How many seconds to wait before treating an onion +# as unreachable +CONNECT_TO_ONION_TIMEOUT = 10 + def location_tuple_to_str(t: Tuple[str, int]) -> str: return f"{t[0]}:{t[1]}" @@ -65,14 +69,15 @@ JM_MESSAGE_TYPES = {"privmsg": 685, "pubmsg": 687} # Used for some control message construction, as detailed below. NICK_PEERLOCATOR_SEPARATOR = ";" -# location_string and nick must be set before sending, +# location_string, nick and network must be set before sending, # otherwise invalid: client_handshake_json = {"app-name": JM_APP_NAME, "directory": False, "location-string": "", "proto-ver": JM_VERSION, "features": {}, - "nick": "" + "nick": "", + "network": "" } # default acceptance false; code must switch it on: @@ -83,6 +88,7 @@ server_handshake_json = {"app-name": JM_APP_NAME, "features": {}, "accepted": False, "nick": "", + "network": "", "motd": "Default MOTD, replace with information for the directory." } @@ -189,6 +195,13 @@ class OnionLineProtocolFactory(protocol.ServerFactory): return del self.peers[peer_location] + def disconnect_inbound_peer(self, inbound_peer_str: str) -> None: + if inbound_peer_str not in self.peers: + log.warn("cannot disconnect peer at {}, not found".format( + inbound_peer_str)) + proto = self.peers[inbound_peer_str] + proto.transport.loseConnection() + def receive_message(self, message: OnionCustomMessage, p: OnionLineProtocol) -> None: self.client.receive_msg(message, network_addr_to_string( @@ -212,6 +225,7 @@ class OnionClientFactory(protocol.ReconnectingClientFactory): def __init__(self, message_receive_callback: Callable, connection_callback: Callable, disconnection_callback: Callable, + message_not_sendable_callback: Callable, directory: bool, mc: 'OnionMessageChannel'): self.proto_client = None @@ -221,6 +235,9 @@ class OnionClientFactory(protocol.ReconnectingClientFactory): self.connection_callback = connection_callback # disconnection the same self.disconnection_callback = disconnection_callback + # a callback that can be fired if we are not able to send messages, + # no args, returns None + self.message_not_sendable_callback = message_not_sendable_callback # is this connection to a directory? self.directory = directory # to keep track of state of overall messagechannel @@ -255,6 +272,11 @@ class OnionClientFactory(protocol.ReconnectingClientFactory): self.disconnection_callback() def send(self, msg: OnionCustomMessage) -> bool: + # we may be sending at the time the counterparty + # disconnected + if not self.proto_client: + self.message_not_sendable_callback() + return False self.proto_client.message(msg) # Unlike the serving protocol, the client protocol # is never in a condition of not knowing the counterparty @@ -430,6 +452,15 @@ class OnionPeer(object): def receive_message(self, message: OnionCustomMessage) -> None: self.messagechannel.receive_msg(message, self.peer_location()) + def notify_message_unsendable(self): + """ Triggered by a failure to send a message on the network, + by the encapsulated ClientFactory. Just used to notify calling + code; no action is triggered. + """ + name = "directory" if self.directory else "peer" + log.warn("Failure to send message to {}: {}.".format( + name, self.peer_location())) + def connect(self) -> None: """ This method is called to connect, over Tor, to the remote peer at the given onion host/port. @@ -442,7 +473,7 @@ class OnionPeer(object): self.factory = OnionClientFactory(self.receive_message, self.register_connection, self.register_disconnection, - self.directory, self.messagechannel) + self.notify_message_unsendable, self.directory, self.messagechannel) if testing_mode: log.debug("{} is making a tcp connection to {}, {}, {},".format( self.messagechannel.self_as_peer.peer_location(), self.hostname, @@ -450,13 +481,37 @@ class OnionPeer(object): self.tcp_connector = reactor.connectTCP(self.hostname, self.port, self.factory) else: + # non-default timeout; needs to be much lower than our + # 'wait at least a minute for the IRC connections to come up', + # which is used for *all* message channels, together. torEndpoint = TCP4ClientEndpoint(reactor, self.socks5_host, - self.socks5_port) + self.socks5_port, + timeout=CONNECT_TO_ONION_TIMEOUT) onionEndpoint = TorSocksEndpoint(torEndpoint, self.hostname, self.port) self.reconnecting_service = ClientService(onionEndpoint, self.factory) + # if we want to actually do something about an unreachable host, + # we have to force t.a.i.ClientService to give up after the timeout: + d = self.reconnecting_service.whenConnected(failAfterFailures=1) + d.addErrback(self.respond_to_connection_failure) self.reconnecting_service.startService() + def respond_to_connection_failure(self, failure): + # the error should be of this type specifically, if the onion + # is down, or was configured wrong: + failure.trap(HostUnreachableError) + # if this is a non-dir reachable peer, we just record + # the failure and explicitly give up: + if not self.directory: + log.info("We failed to connect to peer {}; giving up".format( + self.peer_location())) + self.reconnecting_service.stopService() + else: + # in this case, the still-running ClientService will + # just keep trying: + log.warn("We failed to connect to directory {}; trying " + "again.".format(self.peer_location())) + def register_connection(self) -> None: self.messagechannel.register_connection(self.peer_location(), direction=1) @@ -484,12 +539,14 @@ class OnionPeer(object): if not (self.hostname and self.port > 0): raise OnionPeerConnectionError( "Cannot disconnect without host, port info") - d = self.factory.proto_client.transport.loseConnection() - d.addCallback(self.complete_disconnection) - d.addErrback(log.warn, "Failed to disconnect from peer {}.".format( - self.peer_location())) + if self.factory: + d = self.reconnecting_service.stopService() + d.addCallback(self.complete_disconnection) + else: + self.messagechannel.proto_factory.disconnect_inbound_peer( + self.alternate_location) - def complete_disconnection(self) -> None: + def complete_disconnection(self, r) -> None: log.debug("Disconnected from peer: {}".format(self.peer_location())) self.update_status(PEER_STATUS_DISCONNECTED) self.factory = None @@ -530,6 +587,7 @@ class OnionMessageChannel(MessageChannel): # hostid is a feature to avoid replay attacks across message channels; # TODO investigate, but for now, treat onion-based as one "server". self.hostid = "onion-network" + self.btc_network = configdata["btcnet"] # receives notification that we are shutting down self.give_up = False # for backwards compat: make sure MessageChannel log can refer to @@ -759,9 +817,10 @@ class OnionMessageChannel(MessageChannel): # do not trigger on_welcome event until all directories # configured are ready: self.on_welcome_sent = False + self.directory_wait_counter = 0 self.wait_for_directories_loop = task.LoopingCall( self.wait_for_directories) - self.wait_for_directories_loop.start(10.0) + self.wait_for_directories_loop.start(2.0) def handshake_as_client(self, peer: OnionPeer) -> None: assert peer.status() == PEER_STATUS_CONNECTED @@ -772,6 +831,7 @@ class OnionMessageChannel(MessageChannel): our_hs = copy.deepcopy(client_handshake_json) our_hs["location-string"] = self.self_as_peer.peer_location() our_hs["nick"] = self.nick + our_hs["network"] = self.btc_network our_hs_json = json.dumps(our_hs) log.info("Sending this handshake: {} to peer {}".format( our_hs_json, peer.peer_location())) @@ -780,6 +840,7 @@ class OnionMessageChannel(MessageChannel): def handshake_as_directory(self, peer: OnionPeer, our_hs: dict) -> None: assert peer.status() == PEER_STATUS_CONNECTED + our_hs["network"] = self.btc_network our_hs_json = json.dumps(our_hs) log.info("Sending this handshake as directory: {}".format( our_hs_json)) @@ -1015,10 +1076,12 @@ class OnionMessageChannel(MessageChannel): features = handshake_json["features"] accepted = handshake_json["accepted"] nick = handshake_json["nick"] + net = handshake_json["network"] assert isinstance(proto_max, int) assert isinstance(proto_min, int) assert isinstance(features, dict) assert isinstance(nick, str) + assert isinstance(net, str) except Exception as e: log.warn("Invalid handshake message from: {}," " exception: {}, message: {},ignoring".format( @@ -1029,11 +1092,19 @@ class OnionMessageChannel(MessageChannel): # at all. if not accepted: log.warn("Directory: {} rejected our handshake.".format(peerid)) + # explicitly choose to disconnect (if other side already did, + # this is no-op). + peer.disconnect() return if not (app_name == JM_APP_NAME and is_directory and JM_VERSION \ <= proto_max and JM_VERSION >= proto_min and accepted): log.warn("Handshake from directory is incompatible or " "rejected: {}".format(handshake_json)) + peer.disconnect() + return + if not net == self.btc_network: + log.warn("Handshake from directory is on an incompatible " + "network: {}".format(net)) return # We received a valid, accepting dn-handshake. Update the peer. peer.update_status(PEER_STATUS_HANDSHAKED) @@ -1052,9 +1123,11 @@ class OnionMessageChannel(MessageChannel): features = handshake_json["features"] full_location_string = handshake_json["location-string"] nick = handshake_json["nick"] + net = handshake_json["network"] assert isinstance(proto_ver, int) assert isinstance(features, dict) assert isinstance(nick, str) + assert isinstance(net, str) except Exception as e: log.warn("(not dn) Invalid handshake message from: {}, " "exception: {}, message: {}, ignoring".format( @@ -1066,6 +1139,10 @@ class OnionMessageChannel(MessageChannel): log.warn("Invalid handshake name/version data: {}, from peer: " "{}, rejecting.".format(message, peerid)) accepted = False + if not net == self.btc_network: + log.warn("Handshake from peer is on an incompatible " + "network: {}".format(net)) + accepted = False # If accepted, we should update the peer to have the full # location which in general will not yet be present, so as to # allow publishing their location via `getpeerlist`. Note @@ -1207,10 +1284,27 @@ class OnionMessageChannel(MessageChannel): def wait_for_directories(self) -> None: # Notice this is checking for *handshaked* dps; # the handshake will have been initiated once a - # connection was seen: + # connection was seen. + # Note also that this is *only* called on startup, + # so we are guaranteed to have only directory peers. + if len(self.get_connected_directory_peers()) < len(self.peers): + self.directory_wait_counter += 1 + # < 2*11 = 22 seconds; compare with CONNECT_TO_ONION_TIMEOUT; + # with current vals, we get to try twice before entirely + # giving up. + if self.directory_wait_counter < 11: + return if len(self.get_connected_directory_peers()) == 0: + # at least one handshake must have succeeded, for us + # to continue. + log.error("We failed to connect and handshake with " + "ANY directories; onion messaging is not functioning.") + self.wait_for_directories_loop.stop() return # This is what triggers the start of taker/maker workflows. + # Note that even if the preceding (max) 50 seconds failed to + # connect all our configured dps, we will keep trying and they + # can still be used. if not self.on_welcome_sent: self.on_welcome(self) self.on_welcome_sent = True diff --git a/scripts/joinmarket-qt.py b/scripts/joinmarket-qt.py index 69858b6..aed08df 100755 --- a/scripts/joinmarket-qt.py +++ b/scripts/joinmarket-qt.py @@ -912,10 +912,10 @@ class SpendTab(QWidget): daemon=daemon, gui=True) else: - #This will re-use IRC connections in background (daemon), no restart + #This will re-use message channels in background (daemon), no restart self.clientfactory.getClient().client = self.taker self.clientfactory.getClient().clientStart() - mainWindow.statusBar().showMessage("Connecting to IRC ...") + mainWindow.statusBar().showMessage("Connecting to message channels ...") def takerInfo(self, infotype, infomsg): if infotype == "INFO": diff --git a/test/e2e-coinjoin-test.py b/test/e2e-coinjoin-test.py index 81296af..d5e9818 100644 --- a/test/e2e-coinjoin-test.py +++ b/test/e2e-coinjoin-test.py @@ -32,7 +32,7 @@ log = get_log() # For quicker testing, restrict the range of timelock # addresses to avoid slow load of multiple bots. -# Note: no need to revert this change as ygrunner runs +# Note: no need to revert this change as test runs # in isolation. from jmclient import FidelityBondMixin FidelityBondMixin.TIMELOCK_ERA_YEARS = 2 @@ -62,6 +62,7 @@ def get_onion_messaging_config_regtest(run_num: int, dns=[1], hsd="", mode="TAKE dn_nodes_list = ",".join(dns_to_use) log.info("For node: {}, set dn list to: {}".format(run_num, dn_nodes_list)) cf = {"type": "onion", + "btcnet": "testnet", "socks5_host": "127.0.0.1", "socks5_port": 9050, "tor_control_host": "127.0.0.1",