Browse Source
master5cc1695Disconnection logic fixes, add btcnet to handshake (Adam Gibson)830ac22Allow taker peers to not serve onions + bugfixes. (Adam Gibson)fd550eeOnion-based message channels with directory nodes (Adam Gibson)
21 changed files with 2228 additions and 162 deletions
@ -0,0 +1,209 @@ |
|||||||
|
# HOW TO SETUP ONION MESSAGE CHANNELS IN JOINMARKET |
||||||
|
|
||||||
|
### Contents |
||||||
|
|
||||||
|
1. [Overview](#overview) |
||||||
|
|
||||||
|
2. [Testing, configuring for signet](#testing) |
||||||
|
|
||||||
|
4. [Directory nodes](#directory) |
||||||
|
|
||||||
|
<a name="overview" /> |
||||||
|
|
||||||
|
## Overview |
||||||
|
|
||||||
|
This is a new way for Joinmarket bots to communicate, namely by serving and connecting to Tor onion services. This does not |
||||||
|
introduce any new requirements to your Joinmarket installation, technically, because the use of Payjoin already required the need |
||||||
|
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 |
||||||
|
* most of the transaction negotiation will be happening directly peer to peer, not passed over a central server ( |
||||||
|
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 new `[MESSAGING]` section like this, if they start from scratch: |
||||||
|
|
||||||
|
``` |
||||||
|
[MESSAGING:onion] |
||||||
|
# onion based message channels must have the exact type 'onion' |
||||||
|
# (while the section name above can be MESSAGING:whatever), and there must |
||||||
|
# be only ONE such message channel configured (note the directory servers |
||||||
|
# can be multiple, below): |
||||||
|
type = onion |
||||||
|
|
||||||
|
socks5_host = localhost |
||||||
|
socks5_port = 9050 |
||||||
|
|
||||||
|
# the tor control configuration. |
||||||
|
# for most people running the tor daemon |
||||||
|
# on Linux, no changes are required here: |
||||||
|
tor_control_host = localhost |
||||||
|
# or, to use a UNIX socket |
||||||
|
# tor_control_host = unix:/var/run/tor/control |
||||||
|
tor_control_port = 9051 |
||||||
|
|
||||||
|
# the host/port actually serving the hidden service |
||||||
|
# (note the *virtual port*, that the client uses, |
||||||
|
# is hardcoded to 80): |
||||||
|
onion_serving_host = 127.0.0.1 |
||||||
|
onion_serving_port = 8080 |
||||||
|
|
||||||
|
# directory node configuration |
||||||
|
# |
||||||
|
# This is mandatory for directory nodes (who must also set their |
||||||
|
# own *.onion:port as the only directory in directory_nodes, below), |
||||||
|
# but NOT TO BE USED by non-directory nodes (which is you, unless |
||||||
|
# you know otherwise!), as it will greatly degrade your privacy. |
||||||
|
# (note the default is no value, don't replace it with ""). |
||||||
|
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. |
||||||
|
# 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 - but most importantly, pay attention to: |
||||||
|
|
||||||
|
* 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, 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? |
||||||
|
|
||||||
|
To make outbound Tor connections to other onions in the network, you will need to configure the |
||||||
|
SOCKS5 proxy settings (so, only directory nodes may *not* need this; everyone else does). |
||||||
|
This is identical to what we already do for IRC, except that in this case, we disallow clearnet connections. |
||||||
|
|
||||||
|
#### Running/testing as a maker |
||||||
|
|
||||||
|
A maker will additionally allow *inbound* connections to an onion service. |
||||||
|
This onion service will be ephemeral, that is, it will have a different onion address every time |
||||||
|
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, 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. |
||||||
|
|
||||||
|
As previously mentioned, both of these features - inbound and outbound, to onion, Tor connections - were already in use in Joinmarket. If you want to run/test as a maker bot, but never served an onion service before, it should work fine as long as you have the Tor service running in the background, |
||||||
|
and the default control port 9051 (if not, change that value in the `joinmarket.cfg`, see above). |
||||||
|
|
||||||
|
#### Why not use Lightning based onions? |
||||||
|
|
||||||
|
(*Feel free to skip this section if you don't know what "Lightning based onions" refers to!*). The reason this architecture is |
||||||
|
proposed as an alternative to the previously suggested Lightning-node-based network (see |
||||||
|
[this PR](https://github.com/JoinMarket-Org/joinmarket-clientserver/pull/1000)), is mostly that: |
||||||
|
|
||||||
|
* the latter has a bunch of extra installation and maintenance dependencies (just one example: pyln-client requires coincurve, which we just |
||||||
|
removed) |
||||||
|
* the latter requires establishing a new node "identity" which can be refreshed, but that creates more concern |
||||||
|
* longer term ideas to integrate Lightning payments to the coinjoin workflow (and vice versa!) are not realizable yet |
||||||
|
* using multi-hop onion messaging in the LN network itself is also a way off, and a bit problematic |
||||||
|
|
||||||
|
So the short version is: the Lightning based alternative is certainly feasible, but has a lot more baggage that can't really be justified |
||||||
|
unless we're actually using it for something. |
||||||
|
|
||||||
|
|
||||||
|
<a name="testing" /> |
||||||
|
|
||||||
|
## Testing, and configuring for signet. |
||||||
|
|
||||||
|
This testing section focuses on signet since that will be the less troublesome way of getting involved in tests for |
||||||
|
the non-hardcore JM developer :) |
||||||
|
|
||||||
|
(For the latter, please use the regtest setup by running `test/e2e-coinjoin-test.py` under `pytest`, |
||||||
|
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). |
||||||
|
|
||||||
|
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. |
||||||
|
|
||||||
|
<a name="directory" /> |
||||||
|
|
||||||
|
## Directory nodes |
||||||
|
|
||||||
|
**This last section is for people with a lot of technical knowledge in this area, |
||||||
|
who would like to help by running a directory node. You can ignore it if that does not apply.**. |
||||||
|
|
||||||
|
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. |
||||||
|
|
||||||
|
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 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). |
||||||
|
|
||||||
|
##### Question: How to configure the `directory-nodes` list in our `joinmarket.cfg` for this directory node bot? |
||||||
|
|
||||||
|
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 systemd service: |
||||||
|
|
||||||
|
The most basic bare-bones service seems to work fine here: |
||||||
|
|
||||||
|
``` |
||||||
|
[Unit] |
||||||
|
Description=My JM signet directory node |
||||||
|
Requires=network-online.target |
||||||
|
After=network-online.target |
||||||
|
|
||||||
|
[Service] |
||||||
|
Type=simple |
||||||
|
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 |
||||||
|
``` |
||||||
|
|
||||||
|
... however, you need to kind of 'bootstrap' it the first time. For example: |
||||||
|
|
||||||
|
* run once with systemctl start |
||||||
|
|
||||||
|
* 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. |
||||||
|
|
||||||
|
* `systemctl start` again, now note the onion hostname created from the log or the directory |
||||||
|
|
||||||
|
* set that hostname in `directory_nodes` in `joinmarket.cfg` |
||||||
|
|
||||||
|
* now the service should start correctly |
||||||
|
|
||||||
|
TODO: add some material on network hardening/firewalls here, I guess. |
||||||
|
|
||||||
@ -0,0 +1,340 @@ |
|||||||
|
#! /usr/bin/env python |
||||||
|
'''Creates wallets and yield generators in regtest, |
||||||
|
then runs both them and a JMWalletDaemon instance |
||||||
|
for the taker, injecting the newly created taker |
||||||
|
wallet into it and running sendpayment once. |
||||||
|
Number of ygs is configured in the joinmarket.cfg |
||||||
|
with `regtest-count` in the `ln-onion` type MESSAGING |
||||||
|
section. |
||||||
|
See notes below for more detail on config. |
||||||
|
Run it like: |
||||||
|
pytest \ |
||||||
|
--btcroot=/path/to/bitcoin/bin/ \ |
||||||
|
--btcpwd=123456abcdef --btcconf=/blah/bitcoin.conf \ |
||||||
|
-s test/e2e-coinjoin-test.py |
||||||
|
''' |
||||||
|
from twisted.internet import reactor, defer |
||||||
|
from twisted.web.client import readBody, Headers |
||||||
|
from common import make_wallets |
||||||
|
import pytest |
||||||
|
import random |
||||||
|
import json |
||||||
|
from datetime import datetime |
||||||
|
from jmbase import (get_nontor_agent, BytesProducer, jmprint, |
||||||
|
get_log, stop_reactor) |
||||||
|
from jmclient import (YieldGeneratorBasic, load_test_config, jm_single, |
||||||
|
JMClientProtocolFactory, start_reactor, SegwitWallet, get_mchannels, |
||||||
|
SegwitLegacyWallet, JMWalletDaemon) |
||||||
|
from jmclient.wallet_utils import wallet_gettimelockaddress |
||||||
|
from jmclient.wallet_rpc import api_version_string |
||||||
|
|
||||||
|
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 test runs |
||||||
|
# in isolation. |
||||||
|
from jmclient import FidelityBondMixin |
||||||
|
FidelityBondMixin.TIMELOCK_ERA_YEARS = 2 |
||||||
|
FidelityBondMixin.TIMELOCK_EPOCH_YEAR = datetime.now().year |
||||||
|
FidelityBondMixin.TIMENUMBERS_PER_PUBKEY = 12 |
||||||
|
|
||||||
|
wallet_name = "test-onion-yg-runner.jmdat" |
||||||
|
|
||||||
|
mean_amt = 2.0 |
||||||
|
|
||||||
|
directory_node_indices = [1] |
||||||
|
|
||||||
|
def get_onion_messaging_config_regtest(run_num: int, dns=[1], hsd="", mode="TAKER"): |
||||||
|
""" Sets a onion messaging channel section for a regtest instance |
||||||
|
indexed by `run_num`. The indices to be used as directory nodes |
||||||
|
should be passed as `dns`, as a list of ints. |
||||||
|
""" |
||||||
|
def location_string(directory_node_run_num): |
||||||
|
return "127.0.0.1:" + str( |
||||||
|
8080 + directory_node_run_num) |
||||||
|
if run_num in dns: |
||||||
|
# means *we* are a dn, and dns currently |
||||||
|
# do not use other dns: |
||||||
|
dns_to_use = [location_string(run_num)] |
||||||
|
else: |
||||||
|
dns_to_use = [location_string(a) for a in dns] |
||||||
|
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", |
||||||
|
"tor_control_port": 9051, |
||||||
|
"onion_serving_host": "127.0.0.1", |
||||||
|
"onion_serving_port": 8080 + run_num, |
||||||
|
"hidden_service_dir": "", |
||||||
|
"directory_nodes": dn_nodes_list, |
||||||
|
"regtest_count": "1, 1"} |
||||||
|
if mode == "MAKER": |
||||||
|
cf["serving"] = True |
||||||
|
else: |
||||||
|
cf["serving"] = False |
||||||
|
if run_num in dns: |
||||||
|
# only directories need to use fixed hidden service directories: |
||||||
|
cf["hidden_service_dir"] = hsd |
||||||
|
return cf |
||||||
|
|
||||||
|
|
||||||
|
class RegtestJMClientProtocolFactory(JMClientProtocolFactory): |
||||||
|
i = 1 |
||||||
|
def set_directory_nodes(self, dns): |
||||||
|
# a list of integers representing the directory nodes |
||||||
|
# for this test: |
||||||
|
self.dns = dns |
||||||
|
|
||||||
|
def get_mchannels(self, mode="TAKER"): |
||||||
|
# swaps out any existing onionmc configs |
||||||
|
# in the config settings on startup, for one |
||||||
|
# that's indexed to the regtest counter var: |
||||||
|
default_chans = get_mchannels(mode=mode) |
||||||
|
new_chans = [] |
||||||
|
onion_found = False |
||||||
|
hsd = "" |
||||||
|
for c in default_chans: |
||||||
|
if "type" in c and c["type"] == "onion": |
||||||
|
onion_found = True |
||||||
|
if c["hidden_service_dir"] != "": |
||||||
|
hsd = c["hidden_service_dir"] |
||||||
|
continue |
||||||
|
else: |
||||||
|
new_chans.append(c) |
||||||
|
if onion_found: |
||||||
|
new_chans.append(get_onion_messaging_config_regtest( |
||||||
|
self.i, self.dns, hsd, mode=mode)) |
||||||
|
return new_chans |
||||||
|
|
||||||
|
class JMWalletDaemonT(JMWalletDaemon): |
||||||
|
def check_cookie(self, request): |
||||||
|
if self.auth_disabled: |
||||||
|
return True |
||||||
|
return super().check_cookie(request) |
||||||
|
|
||||||
|
class TWalletRPCManager(object): |
||||||
|
""" Base class for set up of tests of the |
||||||
|
Wallet RPC calls using the wallet_rpc.JMWalletDaemon service. |
||||||
|
""" |
||||||
|
# the port for the jmwallet daemon |
||||||
|
dport = 28183 |
||||||
|
# the port for the ws |
||||||
|
wss_port = 28283 |
||||||
|
|
||||||
|
def __init__(self): |
||||||
|
# a client connnection object which is often but not always |
||||||
|
# instantiated: |
||||||
|
self.client_connector = None |
||||||
|
self.daemon = JMWalletDaemonT(self.dport, self.wss_port, tls=False) |
||||||
|
self.daemon.auth_disabled = True |
||||||
|
# because we sync and start the wallet service manually here |
||||||
|
# (and don't use wallet files yet), we won't have set a wallet name, |
||||||
|
# so we set it here: |
||||||
|
self.daemon.wallet_name = wallet_name |
||||||
|
|
||||||
|
def start(self): |
||||||
|
r, s = self.daemon.startService() |
||||||
|
self.listener_rpc = r |
||||||
|
self.listener_ws = s |
||||||
|
|
||||||
|
def get_route_root(self): |
||||||
|
addr = "http://127.0.0.1:" + str(self.dport) |
||||||
|
addr += api_version_string |
||||||
|
return addr |
||||||
|
|
||||||
|
def stop(self): |
||||||
|
for dc in reactor.getDelayedCalls(): |
||||||
|
dc.cancel() |
||||||
|
d1 = defer.maybeDeferred(self.listener_ws.stopListening) |
||||||
|
d2 = defer.maybeDeferred(self.listener_rpc.stopListening) |
||||||
|
if self.client_connector: |
||||||
|
self.client_connector.disconnect() |
||||||
|
# only fire if everything is finished: |
||||||
|
return defer.gatherResults([d1, d2]) |
||||||
|
|
||||||
|
@defer.inlineCallbacks |
||||||
|
def do_request(self, agent, method, addr, body, handler, token=None): |
||||||
|
if token: |
||||||
|
headers = Headers({"Authorization": ["Bearer " + self.jwt_token]}) |
||||||
|
else: |
||||||
|
headers = None |
||||||
|
response = yield agent.request(method, addr, headers, bodyProducer=body) |
||||||
|
yield self.response_handler(response, handler) |
||||||
|
|
||||||
|
@defer.inlineCallbacks |
||||||
|
def response_handler(self, response, handler): |
||||||
|
body = yield readBody(response) |
||||||
|
# these responses should always be 200 OK. |
||||||
|
#assert response.code == 200 |
||||||
|
# handlers check the body is as expected; no return. |
||||||
|
yield handler(body) |
||||||
|
return True |
||||||
|
|
||||||
|
def test_start_yg_and_taker_setup(setup_onion_ygrunner): |
||||||
|
"""Set up some wallets, for the ygs and 1 taker. |
||||||
|
Then start LN and the ygs in the background, then fire |
||||||
|
a startup of a wallet daemon for the taker who then |
||||||
|
makes a coinjoin payment. |
||||||
|
""" |
||||||
|
if jm_single().config.get("POLICY", "native") == "true": |
||||||
|
walletclass = SegwitWallet |
||||||
|
else: |
||||||
|
# TODO add Legacy |
||||||
|
walletclass = SegwitLegacyWallet |
||||||
|
|
||||||
|
start_bot_num, end_bot_num = [int(x) for x in jm_single().config.get( |
||||||
|
"MESSAGING:onion", "regtest_count").split(",")] |
||||||
|
num_ygs = end_bot_num - start_bot_num |
||||||
|
# specify the number of wallets and bots of each type: |
||||||
|
wallet_services = make_wallets(num_ygs + 1, |
||||||
|
wallet_structures=[[1, 3, 0, 0, 0]] * (num_ygs + 1), |
||||||
|
mean_amt=2.0, |
||||||
|
walletclass=walletclass) |
||||||
|
#the sendpayment bot uses the last wallet in the list |
||||||
|
wallet_service = wallet_services[end_bot_num - 1]['wallet'] |
||||||
|
jmprint("\n\nTaker wallet seed : " + wallet_services[end_bot_num - 1]['seed']) |
||||||
|
# for manual audit if necessary, show the maker's wallet seeds |
||||||
|
# also (note this audit should be automated in future, see |
||||||
|
# test_full_coinjoin.py in this directory) |
||||||
|
jmprint("\n\nMaker wallet seeds: ") |
||||||
|
for i in range(start_bot_num, end_bot_num): |
||||||
|
jmprint("Maker seed: " + wallet_services[i - 1]['seed']) |
||||||
|
jmprint("\n") |
||||||
|
wallet_service.sync_wallet(fast=True) |
||||||
|
ygclass = YieldGeneratorBasic |
||||||
|
|
||||||
|
# As per previous note, override non-default command line settings: |
||||||
|
options = {} |
||||||
|
for x in ["ordertype", "txfee_contribution", "txfee_contribution_factor", |
||||||
|
"cjfee_a", "cjfee_r", "cjfee_factor", "minsize", "size_factor"]: |
||||||
|
options[x] = jm_single().config.get("YIELDGENERATOR", x) |
||||||
|
ordertype = options["ordertype"] |
||||||
|
txfee_contribution = int(options["txfee_contribution"]) |
||||||
|
txfee_contribution_factor = float(options["txfee_contribution_factor"]) |
||||||
|
cjfee_factor = float(options["cjfee_factor"]) |
||||||
|
size_factor = float(options["size_factor"]) |
||||||
|
if ordertype == 'reloffer': |
||||||
|
cjfee_r = options["cjfee_r"] |
||||||
|
# minimum size is such that you always net profit at least 20% |
||||||
|
#of the miner fee |
||||||
|
minsize = max(int(1.2 * txfee_contribution / float(cjfee_r)), |
||||||
|
int(options["minsize"])) |
||||||
|
cjfee_a = None |
||||||
|
elif ordertype == 'absoffer': |
||||||
|
cjfee_a = int(options["cjfee_a"]) |
||||||
|
minsize = int(options["minsize"]) |
||||||
|
cjfee_r = None |
||||||
|
else: |
||||||
|
assert False, "incorrect offertype config for yieldgenerator." |
||||||
|
|
||||||
|
txtype = wallet_service.get_txtype() |
||||||
|
if txtype == "p2wpkh": |
||||||
|
prefix = "sw0" |
||||||
|
elif txtype == "p2sh-p2wpkh": |
||||||
|
prefix = "sw" |
||||||
|
elif txtype == "p2pkh": |
||||||
|
prefix = "" |
||||||
|
else: |
||||||
|
assert False, "Unsupported wallet type for yieldgenerator: " + txtype |
||||||
|
|
||||||
|
ordertype = prefix + ordertype |
||||||
|
|
||||||
|
for i in range(start_bot_num, end_bot_num): |
||||||
|
cfg = [txfee_contribution, cjfee_a, cjfee_r, ordertype, minsize, |
||||||
|
txfee_contribution_factor, cjfee_factor, size_factor] |
||||||
|
wallet_service_yg = wallet_services[i - 1]["wallet"] |
||||||
|
|
||||||
|
wallet_service_yg.startService() |
||||||
|
|
||||||
|
yg = ygclass(wallet_service_yg, cfg) |
||||||
|
clientfactory = RegtestJMClientProtocolFactory(yg, proto_type="MAKER") |
||||||
|
# This ensures that the right rpc/port config is passed into the daemon, |
||||||
|
# for this specific bot: |
||||||
|
clientfactory.i = i |
||||||
|
# This ensures that this bot knows which other bots are directory nodes: |
||||||
|
clientfactory.set_directory_nodes(directory_node_indices) |
||||||
|
nodaemon = jm_single().config.getint("DAEMON", "no_daemon") |
||||||
|
daemon = bool(nodaemon) |
||||||
|
#rs = True if i == num_ygs - 1 else False |
||||||
|
start_reactor(jm_single().config.get("DAEMON", "daemon_host"), |
||||||
|
jm_single().config.getint("DAEMON", "daemon_port"), |
||||||
|
clientfactory, daemon=daemon, rs=False) |
||||||
|
reactor.callLater(1.0, start_test_taker, wallet_services[end_bot_num - 1]['wallet'], end_bot_num, num_ygs) |
||||||
|
reactor.run() |
||||||
|
|
||||||
|
@defer.inlineCallbacks |
||||||
|
def start_test_taker(wallet_service, i, num_ygs): |
||||||
|
# this rpc manager has auth disabled, |
||||||
|
# and the wallet_service is set manually, |
||||||
|
# so no unlock etc. |
||||||
|
mgr = TWalletRPCManager() |
||||||
|
mgr.daemon.services["wallet"] = wallet_service |
||||||
|
# because we are manually setting the wallet_service |
||||||
|
# of the JMWalletDaemon instance, we do not follow the |
||||||
|
# usual flow of `initialize_wallet_service`, we do not set |
||||||
|
# the auth token or start the websocket; so we must manually |
||||||
|
# sync the wallet, including bypassing any restart callback: |
||||||
|
def dummy_restart_callback(msg): |
||||||
|
log.warn("Ignoring rescan request from backend wallet service: " + msg) |
||||||
|
mgr.daemon.services["wallet"].add_restart_callback(dummy_restart_callback) |
||||||
|
mgr.daemon.wallet_name = wallet_name |
||||||
|
mgr.daemon.services["wallet"].startService() |
||||||
|
def get_client_factory(): |
||||||
|
clientfactory = RegtestJMClientProtocolFactory(mgr.daemon.taker, |
||||||
|
proto_type="TAKER") |
||||||
|
clientfactory.i = i |
||||||
|
clientfactory.set_directory_nodes(directory_node_indices) |
||||||
|
return clientfactory |
||||||
|
|
||||||
|
mgr.daemon.get_client_factory = get_client_factory |
||||||
|
# before preparing the RPC call to the wallet daemon, |
||||||
|
# we decide a coinjoin destination, counterparty count and amount. |
||||||
|
# Choosing a destination in the wallet is a bit easier because |
||||||
|
# we can query the mixdepth balance at the end. |
||||||
|
coinjoin_destination = mgr.daemon.services["wallet"].get_internal_addr(4) |
||||||
|
cj_amount = 22000000 |
||||||
|
def n_cps_from_n_ygs(n): |
||||||
|
if n > 4: |
||||||
|
return n - 2 |
||||||
|
if n > 2: |
||||||
|
return 2 |
||||||
|
assert False, "Need at least 3 yield generators to test" |
||||||
|
n_cps = n_cps_from_n_ygs(num_ygs) |
||||||
|
# once the taker is finished we sanity check before |
||||||
|
# shutting down: |
||||||
|
def dummy_taker_finished(res, fromtx=False, |
||||||
|
waittime=0.0, txdetails=None): |
||||||
|
jmprint("Taker is finished") |
||||||
|
# check that the funds have arrived. |
||||||
|
mbal = mgr.daemon.services["wallet"].get_balance_by_mixdepth()[4] |
||||||
|
assert mbal == cj_amount |
||||||
|
jmprint("Funds: {} sats successfully arrived into mixdepth 4.".format(cj_amount)) |
||||||
|
stop_reactor() |
||||||
|
mgr.daemon.taker_finished = dummy_taker_finished |
||||||
|
mgr.start() |
||||||
|
agent = get_nontor_agent() |
||||||
|
addr = mgr.get_route_root() |
||||||
|
addr += "/wallet/" |
||||||
|
addr += mgr.daemon.wallet_name |
||||||
|
addr += "/taker/coinjoin" |
||||||
|
addr = addr.encode() |
||||||
|
body = BytesProducer(json.dumps({"mixdepth": "1", |
||||||
|
"amount_sats": cj_amount, |
||||||
|
"counterparties": str(n_cps), |
||||||
|
"destination": coinjoin_destination}).encode()) |
||||||
|
yield mgr.do_request(agent, b"POST", addr, body, |
||||||
|
process_coinjoin_response) |
||||||
|
|
||||||
|
def process_coinjoin_response(response): |
||||||
|
json_body = json.loads(response.decode("utf-8")) |
||||||
|
print("coinjoin response: {}".format(json_body)) |
||||||
|
|
||||||
|
@pytest.fixture(scope="module") |
||||||
|
def setup_onion_ygrunner(): |
||||||
|
load_test_config() |
||||||
|
jm_single().bc_interface.tick_forward_chain_interval = 10 |
||||||
|
jm_single().bc_interface.simulate_blocks() |
||||||
Loading…
Reference in new issue