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.
 
 
 
 

364 lines
14 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/ln-ygrunner.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, hextobin, bintohex)
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=""):
""" 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 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):
# swaps out any existing lightning configs
# in the config settings on startup, for one
# that's indexed to the regtest counter var:
default_chans = get_mchannels()
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))
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:onion1", "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 = True if nodaemon == 1 else False
#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)
reactor.run()
@defer.inlineCallbacks
def start_test_taker(wallet_service, i):
# this rpc manager has auth disabled,
# and the wallet_service is set manually,
# so no unlock etc.
mgr = TWalletRPCManager()
mgr.daemon.wallet_service = 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.wallet_service.add_restart_callback(dummy_restart_callback)
mgr.daemon.wallet_name = wallet_name
while not mgr.daemon.wallet_service.synced:
mgr.daemon.wallet_service.sync_wallet(fast=True)
mgr.daemon.wallet_service.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 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.wallet_service.get_internal_addr(4)
cj_amount = 22000000
# 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.wallet_service.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": "2",
"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))
def get_addr_and_fund(yg):
""" This function allows us to create
and publish a fidelity bond for a particular
yield generator object after the wallet has reached
a synced state and is therefore ready to serve up
timelock addresses. We create the TL address, fund it,
refresh the wallet and then republish our offers, which
will also publish the new FB.
"""
if not yg.wallet_service.synced:
return
if yg.wallet_service.timelock_funded:
return
addr = wallet_gettimelockaddress(yg.wallet_service.wallet, "2021-11")
print("Got timelockaddress: {}".format(addr))
# pay into it; amount is randomized for now.
# Note that grab_coins already mines 1 block.
fb_amt = random.randint(1, 5)
jm_single().bc_interface.grab_coins(addr, fb_amt)
# we no longer have to run this loop (TODO kill with nonlocal)
yg.wallet_service.timelock_funded = True
# force wallet to check for the new coins so the new
# yg offers will include them:
yg.wallet_service.transaction_monitor()
# publish a new offer:
yg.offerlist = yg.create_my_orders()
yg.fidelity_bond = yg.get_fidelity_bond_template()
jmprint('updated offerlist={}'.format(yg.offerlist))
@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()