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.
 
 
 
 

339 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 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 ygrunner 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",
"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()