You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
333 lines
13 KiB
333 lines
13 KiB
#! /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 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) |
|
import jmclient |
|
from jmclient.wallet_rpc import api_version_string |
|
|
|
log = get_log() |
|
|
|
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) |
|
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 |
|
def setup_onion_ygrunner(monkeypatch): |
|
# For quicker testing, restrict the range of timelock |
|
# addresses to avoid slow load of multiple bots. |
|
monkeypatch.setattr(jmclient.FidelityBondMixin, 'TIMELOCK_ERA_YEARS', 2) |
|
monkeypatch.setattr(jmclient.FidelityBondMixin, 'TIMELOCK_EPOCH_YEAR', datetime.now().year) |
|
load_test_config() |
|
jm_single().bc_interface.tick_forward_chain_interval = 10 |
|
jm_single().bc_interface.simulate_blocks()
|
|
|