From 38f184b231dd81d316583d6ae99d3c63ce564bb5 Mon Sep 17 00:00:00 2001 From: Wukong Date: Fri, 13 Aug 2021 01:16:02 -0700 Subject: [PATCH] Send coin join output to an external cold storage wallet --- jmclient/jmclient/wallet_service.py | 2 +- jmclient/jmclient/wallet_utils.py | 2 +- jmclient/jmclient/yieldgenerator.py | 77 +++++++++++++++++++++++++++-- scripts/yg-privacyenhanced.py | 6 +-- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/jmclient/jmclient/wallet_service.py b/jmclient/jmclient/wallet_service.py index 22046b159..21b004078 100644 --- a/jmclient/jmclient/wallet_service.py +++ b/jmclient/jmclient/wallet_service.py @@ -222,7 +222,7 @@ def start_wallet_monitoring(self, syncresult): if reactor.running: reactor.stop() return - jlog.info("Starting transaction monitor in walletservice") + jlog.info("Starting transaction monitor in walletservice for {}".format(self.get_wallet_name())) self.monitor_loop = task.LoopingCall( self.transaction_monitor) self.monitor_loop.start(5.0) diff --git a/jmclient/jmclient/wallet_utils.py b/jmclient/jmclient/wallet_utils.py index fa0b06206..07fcf8b08 100644 --- a/jmclient/jmclient/wallet_utils.py +++ b/jmclient/jmclient/wallet_utils.py @@ -1413,7 +1413,7 @@ def open_wallet(path, ask_for_password=True, password=None, read_only=False, while True: try: # do not try empty password, assume unencrypted on empty password - pwd = get_password("Enter passphrase to decrypt wallet: ") or None + pwd = get_password("Enter passphrase to decrypt wallet {}: ".format(path)) or None storage = Storage(path, password=pwd, read_only=read_only) except StoragePasswordError: jmprint("Wrong password, try again.", "warning") diff --git a/jmclient/jmclient/yieldgenerator.py b/jmclient/jmclient/yieldgenerator.py index 4443765c2..59c518ab4 100644 --- a/jmclient/jmclient/yieldgenerator.py +++ b/jmclient/jmclient/yieldgenerator.py @@ -5,6 +5,7 @@ import time import abc import base64 +from jmclient.wallet import WatchonlyMixin from twisted.python.log import startLogging from optparse import OptionParser from jmbase import get_log @@ -80,10 +81,16 @@ class YieldGeneratorBasic(YieldGenerator): It will often (but not always) reannounce orders after transactions, thus is somewhat suboptimal in giving more information to spies. """ - def __init__(self, wallet_service, offerconfig): + def __init__(self, wallet_service, offerconfig, cswallet_service = None, csconfig = None): # note the randomizing entries are ignored in this base class: self.txfee, self.cjfee_a, self.cjfee_r, self.ordertype, self.minsize, \ self.txfee_factor, self.cjfee_factor, self.size_factor = offerconfig + + if cswallet_service: + assert isinstance(cswallet_service, WalletService) + self.cs_min_balance, self.cs_min_txsize, self.cs_mixdepth = csconfig + self.cswallet_service = cswallet_service + super().__init__(wallet_service) def create_my_orders(self): @@ -255,13 +262,41 @@ def select_input_mixdepth(self, available, offer, amount): available = sorted(available.items(), key=lambda entry: entry[0]) return available[0][0] + def should_use_cswallet(self, input_mixdepth, amount): + if not self.cswallet_service: + return False + + if input_mixdepth != self.cs_mixdepth: + jlog.debug("input mixdepth {} dosen't match cs_mixdepth {}".format( + input_mixdepth, self.cs_mixdepth)) + return False + + if amount < self.cs_min_txsize: + jlog.debug("coin join size {} is less than cs_min_txsize {}".format( + amount, self.cs_min_txsize)) + return False + + # Check if the wallet balance can maintain the min balance after this coin join + total_wallet_balance = sum(self.wallet_service.get_balance_by_mixdepth().values()) + if total_wallet_balance - amount < self.cs_min_balance: + jlog.debug("total_wallet_balance - amount {} is less than cs_min_balance {}".format( + total_wallet_balance - amount, self.cs_min_balance)) + return False + else: + jlog.debug("sending coin join output to cold storage wallet {}".format( + self.cswallet_service.get_wallet_name())) + return True + def select_output_address(self, input_mixdepth, offer, amount): """Returns the address to which the mixed output should be sent for an order spending from the given input mixdepth. Can return None if there is no suitable output, in which case the order is aborted.""" - cjoutmix = (input_mixdepth + 1) % (self.wallet_service.mixdepth + 1) - return self.wallet_service.get_internal_addr(cjoutmix) + if self.should_use_cswallet(input_mixdepth, amount): + return self.cswallet_service.get_internal_addr(WatchonlyMixin.WATCH_ONLY_MIXDEPTH) + else: + cjoutmix = (input_mixdepth + 1) % (self.wallet_service.mixdepth + 1) + return self.wallet_service.get_internal_addr(cjoutmix) def ygmain(ygclass, nickserv_password='', gaplimit=6): @@ -299,6 +334,18 @@ def ygmain(ygclass, nickserv_password='', gaplimit=6): parser.add_option('-j', '--cjfee-factor', action='store', type='float', dest='cjfee_factor', default=None, help='variance around the average fee, decimal fraction') + parser.add_option('-c', '--cswallet', action='store', type='string', + dest='cswallet_name', default=None, + help='a watch only cold storage wallet to send coinjoin output to') + parser.add_option('-d', '--cs-mixdepth', action='store', type='int', + dest='cs_mixdepth', default=4, + help='only send coinjoin output to cold storage from this mixdepth') + parser.add_option('-x', '--cs-min-txsize', action='store', type='int', + dest='cs_min_txsize', default=100000000, + help='minimum coinjoin size in satoshis for sending to cold storage') + parser.add_option('-b', '--cs-min-balance', action='store', type='int', + dest='cs_min_balance', default=500000000, + help='minimum balalance in satoshis to keep locally') parser.add_option('-p', '--password', action='store', type='string', dest='password', default=nickserv_password, help='irc nickserv password') @@ -334,6 +381,12 @@ def ygmain(ygclass, nickserv_password='', gaplimit=6): txfee_factor = float(options["txfee_factor"]) cjfee_factor = float(options["cjfee_factor"]) size_factor = float(options["size_factor"]) + + cswallet_name = options["cswallet_name"] + cs_min_balance = int(options["cs_min_balance"]) + cs_min_txsize = int(options["cs_min_txsize"]) + cs_mixdepth = int(options["cs_mixdepth"]) + if ordertype == 'reloffer': cjfee_r = options["cjfee_r"] # minimum size is such that you always net profit at least 20% @@ -366,6 +419,20 @@ def ygmain(ygclass, nickserv_password='', gaplimit=6): wallet_service.sync_wallet(fast=not options["recoversync"]) wallet_service.startService() + if cswallet_name: + cswallet_path = get_wallet_path(cswallet_name, None) + cswallet = open_test_wallet_maybe( + cswallet_path, cswallet_name, 0, + wallet_password_stdin=options["wallet_password_stdin"], + gap_limit=options["gaplimit"]) + + cswallet_service = WalletService(cswallet) + while not cswallet_service.synced: + cswallet_service.sync_wallet(fast=not options["recoversync"]) + cswallet_service.startService() + else: + cswallet_service = None + txtype = wallet_service.get_txtype() if txtype == "p2wpkh": prefix = "sw0" @@ -382,7 +449,9 @@ def ygmain(ygclass, nickserv_password='', gaplimit=6): maker = ygclass(wallet_service, [txfee, cjfee_a, cjfee_r, ordertype, minsize, txfee_factor, - cjfee_factor, size_factor]) + cjfee_factor, size_factor], + cswallet_service, [cs_min_balance, cs_min_txsize, cs_mixdepth]) + jlog.info('starting yield generator') clientfactory = JMClientProtocolFactory(maker, proto_type="MAKER") if jm_single().config.get("SNICKER", "enabled") == "true": diff --git a/scripts/yg-privacyenhanced.py b/scripts/yg-privacyenhanced.py index be6f142ab..7341e5253 100755 --- a/scripts/yg-privacyenhanced.py +++ b/scripts/yg-privacyenhanced.py @@ -20,9 +20,9 @@ class YieldGeneratorPrivacyEnhanced(YieldGeneratorBasic): - def __init__(self, wallet_service, offerconfig): - super().__init__(wallet_service, offerconfig) - + def __init__(self, wallet_service, offerconfig, cswallet_service = None, csconfig = None): + super().__init__(wallet_service, offerconfig, cswallet_service, csconfig) + def select_input_mixdepth(self, available, offer, amount): """Mixdepths are in cyclic order and we select the mixdepth to maximize the largest interval of non-available mixdepths by choosing