def on_auth_received(self, nick, offer, commitment, cr, amount, kphex): if self.authmal: jmprint("Counterparty commitment rejected maliciously", "debug") return (False, ) return super(DeterministicMaliciousYieldGenerator, self).on_auth_received(nick, offer, commitment, cr, amount, kphex)
def on_tx_received(self, nick, txhex, offerinfo): if self.txmal: if random.randint(1, 100) < self.mfrac: jmprint("Counterparty tx rejected maliciously", "debug") return (False, "malicious tx rejection") return super(MaliciousYieldGenerator, self).on_tx_received(nick, txhex, offerinfo)
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))
def test_start_payjoin_server(setup_payjoin_server): # set up the wallet that the server owns, and the wallet for # the sender too (print the seed): if jm_single().config.get("POLICY", "native") == "true": walletclass = SegwitWallet else: walletclass = SegwitLegacyWallet wallet_services = make_wallets(2, wallet_structures=[[1, 3, 0, 0, 0]] * 2, mean_amt=2, walletclass=walletclass) #the server bot uses the first wallet, the sender the second server_wallet_service = wallet_services[0]['wallet'] jmprint("\n\nTaker wallet seed : " + wallet_services[1]['seed']) jmprint("\n") server_wallet_service.sync_wallet(fast=True) site = Site(PayjoinServer(server_wallet_service)) # TODO for now, just sticking with TLS test as non-encrypted # is unlikely to be used, but add that option. reactor.listenSSL(8080, site, contextFactory=get_ssl_context()) #endpoint = endpoints.TCP4ServerEndpoint(reactor, 8080) #endpoint.listen(site) reactor.run()
def on_auth_received(self, nick, offer, commitment, cr, amount, kphex): if self.authmal: if random.randint(1, 100) < self.mfrac: jmprint("Counterparty commitment rejected maliciously", "debug") return (False,) return super(MaliciousYieldGenerator, self).on_auth_received(nick, offer, commitment, cr, amount, kphex)
def get_utxo_info(upriv): """Verify that the input string parses correctly as (utxo, priv) and return that. """ try: u, priv = upriv.split(',') u = u.strip() priv = priv.strip() txid, n = u.split(':') assert len(txid) == 64 assert len(n) in range(1, 4) n = int(n) assert n in range(256) except: #not sending data to stdout in case privkey info jmprint("Failed to parse utxo information for utxo", "error") raise try: hexpriv = btc.from_wif_privkey(priv, vbyte=get_p2pk_vbyte()) except: jmprint( "failed to parse privkey, make sure it's WIF compressed format.", "error") raise return u, priv
def get_utxo_info(upriv, utxo_binary=False): """Verify that the input string parses correctly as (utxo, priv) and return that. If `utxo_binary` is true, the first element of that return tuple is the standard internal form (txid-in-binary, index-as-int). """ try: u, priv = upriv.split(',') u = u.strip() priv = priv.strip() success, utxo_bin = utxostr_to_utxo(u) assert success, utxo except: #not sending data to stdout in case privkey info jmprint("Failed to parse utxo information for utxo", "error") raise try: # see note below for why keytype is ignored, and note that # this calls read_privkey to validate. raw, _ = BTCEngine.wif_to_privkey(priv) except: jmprint("failed to parse privkey, make sure it's WIF compressed format.", "error") raise utxo_to_return = utxo_bin if utxo_binary else u return utxo_to_return, priv
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()
def start_snicker_server_and_tor(self): """ Packages the startup of the receiver side. """ self.server = SNICKERServer() self.site = Site(self.server) self.site.displayTracebacks = False jmprint("Attempting to start onion service on port: " + str(self.port) + " ...") self.start_tor()
def estimate_fee_per_kb(self, N): if super(ElectrumInterface, self).fee_per_kb_has_been_manually_set(N): return int(random.uniform(N * float(0.8), N * float(1.2))) fee_info = self.get_from_electrum('blockchain.estimatefee', N, blocking=True) jmprint('got fee info result: ' + str(fee_info), "debug") fee = fee_info.get('result') fee_per_kb_sat = int(float(fee) * 100000000) return fee_per_kb_sat
def tx_watcher(self, txd, unconfirmfun, confirmfun, spentfun, c, n): """Called at a polling interval, checks if the given deserialized transaction (which must be fully signed) is (a) broadcast, (b) confirmed and (c) spent from. (c, n ignored in electrum version, just supports registering first confirmation). TODO: There is no handling of conflicts here. """ txid = btc.txhash(btc.serialize(txd)) wl = self.tx_watcher_loops[txid] #first check if in mempool (unconfirmed) #choose an output address for the query. Filter out #p2pkh addresses, assume p2sh (thus would fail to find tx on #some nonstandard script type) addr = None for i in range(len(txd['outs'])): if not btc.is_p2pkh_script(txd['outs'][i]['script']): addr = btc.script_to_address(txd['outs'][i]['script'], get_p2sh_vbyte()) break if not addr: log.error("Failed to find any p2sh output, cannot be a standard " "joinmarket transaction, fatal error!") reactor.stop() return unconftxs_res = self.get_from_electrum( 'blockchain.address.get_mempool', addr, blocking=True).get('result') unconftxs = [str(t['tx_hash']) for t in unconftxs_res] if not wl[1] and txid in unconftxs: jmprint("Tx: " + str(txid) + " seen on network.", "info") unconfirmfun(txd, txid) wl[1] = True return conftx = self.get_from_electrum('blockchain.address.listunspent', addr, blocking=True).get('result') conftxs = [str(t['tx_hash']) for t in conftx] if not wl[2] and len(conftxs) and txid in conftxs: jmprint("Tx: " + str(txid) + " is confirmed.", "info") confirmfun(txd, txid, 1) wl[2] = True #Note we do not stop the monitoring loop when #confirmations occur, since we are also monitoring for spending. return if not spentfun or wl[3]: return
def display_rescan_message_and_system_exit(self, restart_cb): #TODO using system exit here should be avoided as it makes the code # harder to understand and reason about #theres also a sys.exit() in BitcoinCoreInterface.import_addresses() #perhaps have sys.exit() placed inside the restart_cb that only # CLI scripts will use if self.bci.__class__ == BitcoinCoreInterface: #Exit conditions cannot be included in tests restart_msg = ( "Use `bitcoin-cli rescanblockchain` if you're " "recovering an existing wallet from backup seed\n" "Otherwise just restart this joinmarket application.") if restart_cb: restart_cb(restart_msg) else: jmprint(restart_msg, "important") sys.exit(EXIT_SUCCESS)
def test_start_ygs(setup_ygrunner, num_ygs, wallet_structures, mean_amt, malicious, deterministic): """Set up some wallets, for the ygs and 1 sp. Then start the ygs in background and publish the seed of the sp wallet for easy import into -qt """ if jm_single().config.get("POLICY", "native") == "true": walletclass = SegwitWallet else: # TODO add Legacy walletclass = SegwitLegacyWallet wallet_services = make_wallets(num_ygs + 1, wallet_structures=wallet_structures, mean_amt=mean_amt, walletclass=walletclass) #the sendpayment bot uses the last wallet in the list wallet_service = wallet_services[num_ygs]['wallet'] jmprint("\n\nTaker wallet seed : " + wallet_services[num_ygs]['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(num_ygs): jmprint("Maker seed: " + wallet_services[i]['seed']) jmprint("\n") wallet_service.sync_wallet(fast=True) txfee = 1000 cjfee_a = 4200 cjfee_r = '0.001' ordertype = 'swreloffer' minsize = 100000 ygclass = YieldGeneratorBasic if malicious: if deterministic: ygclass = DeterministicMaliciousYieldGenerator else: ygclass = MaliciousYieldGenerator for i in range(num_ygs): cfg = [txfee, cjfee_a, cjfee_r, ordertype, minsize] wallet_service_yg = wallet_services[i]["wallet"] wallet_service_yg.startService() yg = ygclass(wallet_service_yg, cfg) if malicious: yg.set_maliciousness(malicious, mtype="tx") clientfactory = JMClientProtocolFactory(yg, proto_type="MAKER") 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=rs)
def update_commitments(commitment=None, external_to_remove=None, external_to_add=None): """Optionally add the commitment commitment to the list of 'used', and optionally remove the available external commitment whose key value is the utxo in external_to_remove, persist updated entries to disk. """ c = {} if os.path.isfile(PODLE_COMMIT_FILE): with open(PODLE_COMMIT_FILE, "rb") as f: try: c = json.loads(f.read().decode('utf-8')) except ValueError: #pragma: no cover #Exit conditions cannot be included in tests. jmprint( "the file: " + PODLE_COMMIT_FILE + " is not valid json.", "error") sys.exit(0) if 'used' in c: commitments = c['used'] else: commitments = [] if 'external' in c: external = c['external'] else: external = {} if commitment: commitments.append(commitment) #remove repeats commitments = list(set(commitments)) if external_to_remove: external = { k: v for k, v in external.items() if k not in external_to_remove } if external_to_add: external.update(external_to_add) to_write = {} to_write['used'] = commitments to_write['external'] = external with open(PODLE_COMMIT_FILE, "wb") as f: f.write(json.dumps(to_write, indent=4).encode('utf-8'))
def read_from_podle_file(): """ Returns used commitment list and external commitments dict struct currently stored in PODLE_COMMIT_FILE. """ if os.path.isfile(PODLE_COMMIT_FILE): with open(PODLE_COMMIT_FILE, "rb") as f: try: c = json.loads(f.read().decode('utf-8')) except ValueError: #pragma: no cover #Exit conditions cannot be included in tests. jmprint("the file: " + PODLE_COMMIT_FILE + " is not valid json.", "error") sys.exit(EXIT_FAILURE) if 'used' not in c.keys() or 'external' not in c.keys(): raise PoDLEError("Incorrectly formatted file: " + PODLE_COMMIT_FILE) used = [hextobin(x) for x in c["used"]] external = external_dict_from_file(c["external"]) return (used, external) return ([], {})
def validate_utxo_data(utxo_datas, retrieve=False, utxo_address_type="p2wpkh"): """For each (utxo, privkey), first convert the privkey and convert to address, then use the blockchain instance to look up the utxo and check that its address field matches. If retrieve is True, return the set of utxos and their values. """ results = [] for u, priv in utxo_datas: success, utxostr = utxo_to_utxostr(u) if not success: jmprint("Invalid utxo format: " + str(u), "error") sys.exit(EXIT_FAILURE) jmprint('validating this utxo: ' + utxostr, "info") # as noted in `ImportWalletMixin` code comments, there is not # yet a functional auto-detection of key type from WIF, hence # the need for this additional switch: if utxo_address_type == "p2wpkh": engine = BTC_P2WPKH elif utxo_address_type == "p2sh-p2wpkh": engine = BTC_P2SH_P2WPKH elif utxo_address_type == "p2pkh": engine = BTC_P2PKH else: raise Exception("Invalid argument: " + str(utxo_address_type)) rawpriv, _ = BTCEngine.wif_to_privkey(priv) addr = engine.privkey_to_address(rawpriv) jmprint('claimed address: ' + addr, "info") res = jm_single().bc_interface.query_utxo_set([u]) if len(res) != 1 or None in res: jmprint("utxo not found on blockchain: " + utxostr, "error") return False returned_addr = engine.script_to_address(res[0]['script']) if returned_addr != addr: return print_failed_addr_match(utxostr, addr, returned_addr) if retrieve: results.append((u, res[0]['value'])) jmprint('all utxos validated OK', "success") if retrieve: return results return True
def get_utxo_info(upriv): """Verify that the input string parses correctly as (utxo, priv) and return that. """ try: u, priv = upriv.split(',') u = u.strip() priv = priv.strip() success, utxo = utxostr_to_utxo(u) assert success, utxo except: #not sending data to stdout in case privkey info jmprint("Failed to parse utxo information for utxo", "error") raise try: # see note below for why keytype is ignored, and note that # this calls read_privkey to validate. raw, _ = BTCEngine.wif_to_privkey(priv) except: jmprint( "failed to parse privkey, make sure it's WIF compressed format.", "error") raise return u, priv
randomize_cjfee = int(random.uniform(float(self.cjfee_a) * (1 - float(self.cjfee_factor)), float(self.cjfee_a) * (1 + float(self.cjfee_factor)))) randomize_cjfee = randomize_cjfee + randomize_txfee else: randomize_cjfee = random.uniform(float(f) * (1 - float(self.cjfee_factor)), float(f) * (1 + float(self.cjfee_factor))) randomize_cjfee = "{0:.6f}".format(randomize_cjfee) # round to 6 decimals order = {'oid': 0, 'ordertype': self.ordertype, 'minsize': randomize_minsize, 'maxsize': randomize_maxsize, 'txfee': randomize_txfee, 'cjfee': str(randomize_cjfee)} # sanity check assert order['minsize'] >= 0 assert order['maxsize'] > 0 assert order['minsize'] <= order['maxsize'] if order['ordertype'] in ['swreloffer', 'sw0reloffer']: while order['txfee'] >= (float(order['cjfee']) * order['minsize']): order['txfee'] = int(order['txfee'] / 2) jlog.info('Warning: too high txfee to be profitable, halfing it to: ' + str(order['txfee'])) return [order] if __name__ == "__main__": ygmain(YieldGeneratorPrivacyEnhanced, nickserv_password='') jmprint('done', "success")
def main(): parser = OptionParser( usage= 'usage: %prog [options] walletname hex-tx input-index output-index net-transfer', description=description) add_base_options(parser) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', help='mixdepth/account to spend from, default=0', default=0) parser.add_option('-g', '--gap-limit', action='store', type='int', dest='gaplimit', default=6, help='gap limit for Joinmarket wallet, default 6.') parser.add_option( '-n', '--no-upload', action='store_true', dest='no_upload', default=False, help="if set, we don't upload the new proposal to the servers") parser.add_option( '-f', '--txfee', action='store', type='int', dest='txfee', default=-1, help='Bitcoin miner tx_fee to use for transaction(s). A number higher ' 'than 1000 is used as "satoshi per KB" tx fee. A number lower than that ' 'uses the dynamic fee estimation of your blockchain provider as ' 'confirmation target. This temporarily overrides the "tx_fees" setting ' 'in your joinmarket.cfg. Works the same way as described in it. Check ' 'it for examples.') parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) (options, args) = parser.parse_args() snicker_plugin = JMPluginService("SNICKER") load_program_config(config_path=options.datadir, plugin_services=[snicker_plugin]) if len(args) != 5: jmprint("Invalid arguments, see --help") sys.exit(EXIT_ARGERROR) wallet_name, hextx, input_index, output_index, net_transfer = args input_index, output_index, net_transfer = [ int(x) for x in [input_index, output_index, net_transfer] ] check_regtest() # If tx_fees are set manually by CLI argument, override joinmarket.cfg: if int(options.txfee) > 0: jm_single().config.set("POLICY", "tx_fees", str(options.txfee)) max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) wallet_path = get_wallet_path(wallet_name, None) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) if wallet_service.rpc_error: sys.exit(EXIT_FAILURE) snicker_plugin.start_plugin_logging(wallet_service) # in this script, we need the wallet synced before # logic processing for some paths, so do it now: while not wallet_service.synced: wallet_service.sync_wallet(fast=not options.recoversync) # the sync call here will now be a no-op: wallet_service.startService() # now that the wallet is available, we can construct a proposal # before encrypting it: originating_tx = btc.CMutableTransaction.deserialize(hextobin(hextx)) txid1 = originating_tx.GetTxid()[::-1] # the proposer wallet needs to choose a single utxo, from his selected # mixdepth, that is bigger than the output amount of tx1 at the given # index. fee_est = estimate_tx_fee(2, 3, txtype=wallet_service.get_txtype()) amt_required = originating_tx.vout[output_index].nValue + fee_est prop_utxo_dict = wallet_service.select_utxos(options.mixdepth, amt_required) prop_utxos = list(prop_utxo_dict) prop_utxo_vals = [prop_utxo_dict[x] for x in prop_utxos] # get the private key for that utxo priv = wallet_service.get_key_from_addr( wallet_service.script_to_addr(prop_utxo_vals[0]['script'])) # construct the arguments for the snicker proposal: our_input_utxos = [ btc.CMutableTxOut(x['value'], x['script']) for x in prop_utxo_vals ] # destination must be a different mixdepth: prop_destn_spk = wallet_service.get_new_script( (options.mixdepth + 1) % (wallet_service.mixdepth + 1), 1) change_spk = wallet_service.get_new_script(options.mixdepth, 1) their_input = (txid1, output_index) # we also need to extract the pubkey of the chosen input from # the witness; we vary this depending on our wallet type: pubkey, msg = btc.extract_pubkey_from_witness(originating_tx, input_index) if not pubkey: log.error("Failed to extract pubkey from transaction: {}".format(msg)) sys.exit(EXIT_FAILURE) encrypted_proposal = wallet_service.create_snicker_proposal( prop_utxos, their_input, our_input_utxos, originating_tx.vout[output_index], net_transfer, fee_est, priv, pubkey, prop_destn_spk, change_spk, version_byte=1) + b"," + bintohex(pubkey).encode('utf-8') if options.no_upload: jmprint(encrypted_proposal.decode("utf-8")) sys.exit(EXIT_SUCCESS) nodaemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = True if nodaemon == 1 else False snicker_client = SNICKERPostingClient([encrypted_proposal]) servers = jm_single().config.get("SNICKER", "servers").split(",") snicker_pf = SNICKERClientProtocolFactory(snicker_client, servers) start_reactor(jm_single().config.get("DAEMON", "daemon_host"), jm_single().config.getint("DAEMON", "daemon_port"), None, snickerfactory=snicker_pf, daemon=daemon)
def default_info_callback(self, msg): jmprint(msg)
def default_user_info_callback(self, msg): """ Info level message print to command line. """ jmprint(msg)
def main(): parser = OptionParser( usage= 'usage: %prog [options] [txid:n]', description="Adds one or more utxos to the list that can be used to make " "commitments for anti-snooping. Note that this utxo, and its " "PUBkey, will be revealed to makers, so consider the privacy " "implication. " "It may be useful to those who are having trouble making " "coinjoins due to several unsuccessful attempts (especially " "if your joinmarket wallet is new). " "'Utxo' means unspent transaction output, it must not " "already be spent. " "The options -w, -r and -R offer ways to load these utxos " "from a file or wallet. " "If you enter a single utxo without these options, you will be " "prompted to enter the private key here - it must be in " "WIF compressed format. " "BE CAREFUL about handling private keys! " "Don't do this in insecure environments. " "Also note this ONLY works for standard (p2pkh or p2sh-p2wpkh) utxos." ) parser.add_option( '-r', '--read-from-file', action='store', type='str', dest='in_file', help='name of plain text csv file containing utxos, one per line, format: ' 'txid:N, WIF-compressed-privkey' ) parser.add_option( '-R', '--read-from-json', action='store', type='str', dest='in_json', help='name of json formatted file containing utxos with private keys, as ' 'output from "python wallet-tool.py -p walletname showutxos"' ) parser.add_option( '-w', '--load-wallet', action='store', type='str', dest='loadwallet', help='name of wallet from which to load utxos and use as commitments.' ) parser.add_option( '-g', '--gap-limit', action='store', type='int', dest='gaplimit', default = 6, help='Only to be used with -w; gap limit for Joinmarket wallet, default 6.' ) parser.add_option( '-M', '--max-mixdepth', action='store', type='int', dest='maxmixdepth', default=5, help='Only to be used with -w; number of mixdepths for wallet, default 5.' ) parser.add_option( '-d', '--delete-external', action='store_true', dest='delete_ext', help='deletes the current list of external commitment utxos', default=False ) parser.add_option( '-v', '--validate-utxos', action='store_true', dest='validate', help='validate the utxos and pubkeys provided against the blockchain', default=False ) parser.add_option( '-o', '--validate-only', action='store_true', dest='vonly', help='only validate the provided utxos (file or command line), not add', default=False ) parser.add_option('--fast', action='store_true', dest='fastsync', default=False, help=('choose to do fast wallet sync, only for Core and ' 'only for previously synced wallet')) (options, args) = parser.parse_args() load_program_config() #TODO; sort out "commit file location" global so this script can #run without this hardcoding: utxo_data = [] if options.delete_ext: other = options.in_file or options.in_json or options.loadwallet if len(args) > 0 or other: if input("You have chosen to delete commitments, other arguments " "will be ignored; continue? (y/n)") != 'y': jmprint("Quitting", "warning") sys.exit(0) c, e = get_podle_commitments() jmprint(pformat(e), "info") if input( "You will remove the above commitments; are you sure? (y/n): ") != 'y': jmprint("Quitting", "warning") sys.exit(0) update_commitments(external_to_remove=e) jmprint("Commitments deleted.", "important") sys.exit(0) #Three options (-w, -r, -R) for loading utxo and privkey pairs from a wallet, #csv file or json file. if options.loadwallet: wallet_path = get_wallet_path(options.loadwallet, None) wallet = open_wallet(wallet_path, gap_limit=options.gaplimit) while not jm_single().bc_interface.wallet_synced: sync_wallet(wallet, fast=options.fastsync) # minor note: adding a utxo from an external wallet for commitments, we # default to not allowing disabled utxos to avoid a privacy leak, so the # user would have to explicitly enable. for md, utxos in wallet.get_utxos_by_mixdepth_().items(): for (txid, index), utxo in utxos.items(): txhex = binascii.hexlify(txid).decode('ascii') + ':' + str(index) wif = wallet.get_wif_path(utxo['path']) utxo_data.append((txhex, wif)) elif options.in_file: with open(options.in_file, "rb") as f: utxo_info = f.readlines() for ul in utxo_info: ul = ul.rstrip() if ul: u, priv = get_utxo_info(ul) if not u: quit(parser, "Failed to parse utxo info: " + str(ul)) utxo_data.append((u, priv)) elif options.in_json: if not os.path.isfile(options.in_json): jmprint("File: " + options.in_json + " not found.", "error") sys.exit(0) with open(options.in_json, "rb") as f: try: utxo_json = json.loads(f.read()) except: jmprint("Failed to read json from " + options.in_json, "error") sys.exit(0) for u, pva in iteritems(utxo_json): utxo_data.append((u, pva['privkey'])) elif len(args) == 1: u = args[0] priv = input( 'input private key for ' + u + ', in WIF compressed format : ') u, priv = get_utxo_info(','.join([u, priv])) if not u: quit(parser, "Failed to parse utxo info: " + u) utxo_data.append((u, priv)) else: quit(parser, 'Invalid syntax') if options.validate or options.vonly: sw = False if jm_single().config.get("POLICY", "segwit") == "false" else True if not validate_utxo_data(utxo_data, segwit=sw): quit(parser, "Utxos did not validate, quitting") if options.vonly: sys.exit(0) #We are adding utxos to the external list assert len(utxo_data) add_ext_commitments(utxo_data)
def render_POST(self, request): """ The sender will use POST to send the initial payment transaction. """ jmprint("The server got this POST request: ") print(request) print(request.method) print(request.uri) print(request.args) print(request.path) print(request.content) proposed_tx = request.content assert isinstance(proposed_tx, BytesIO) payment_psbt_base64 = proposed_tx.read() payment_psbt = btc.PartiallySignedTransaction.from_base64( payment_psbt_base64) all_receiver_utxos = self.wallet_service.get_all_utxos() # TODO is there a less verbose way to get any 2 utxos from the dict? receiver_utxos_keys = list(all_receiver_utxos.keys())[:2] receiver_utxos = {k: v for k, v in all_receiver_utxos.items( ) if k in receiver_utxos_keys} # receiver will do other checks as discussed above, including payment # amount; as discussed above, this is out of the scope of this PSBT test. # construct unsigned tx for payjoin-psbt: payjoin_tx_inputs = [(x.prevout.hash[::-1], x.prevout.n) for x in payment_psbt.unsigned_tx.vin] payjoin_tx_inputs.extend(receiver_utxos.keys()) # find payment output and change output pay_out = None change_out = None for o in payment_psbt.unsigned_tx.vout: jm_out_fmt = {"value": o.nValue, "address": str(btc.CCoinAddress.from_scriptPubKey( o.scriptPubKey))} if o.nValue == payment_amt: assert pay_out is None pay_out = jm_out_fmt else: assert change_out is None change_out = jm_out_fmt # we now know there were two outputs and know which is payment. # bump payment output with our input: outs = [pay_out, change_out] our_inputs_val = sum([v["value"] for _, v in receiver_utxos.items()]) pay_out["value"] += our_inputs_val print("we bumped the payment output value by: ", our_inputs_val) print("It is now: ", pay_out["value"]) unsigned_payjoin_tx = btc.make_shuffled_tx(payjoin_tx_inputs, outs, version=payment_psbt.unsigned_tx.nVersion, locktime=payment_psbt.unsigned_tx.nLockTime) print("we created this unsigned tx: ") print(btc.human_readable_transaction(unsigned_payjoin_tx)) # to create the PSBT we need the spent_outs for each input, # in the right order: spent_outs = [] for i, inp in enumerate(unsigned_payjoin_tx.vin): input_found = False for j, inp2 in enumerate(payment_psbt.unsigned_tx.vin): if inp.prevout == inp2.prevout: spent_outs.append(payment_psbt.inputs[j].utxo) input_found = True break if input_found: continue # if we got here this input is ours, we must find # it from our original utxo choice list: for ru in receiver_utxos.keys(): if (inp.prevout.hash[::-1], inp.prevout.n) == ru: spent_outs.append( self.wallet_service.witness_utxos_to_psbt_utxos( {ru: receiver_utxos[ru]})[0]) input_found = True break # there should be no other inputs: assert input_found r_payjoin_psbt = self.wallet_service.create_psbt_from_tx(unsigned_payjoin_tx, spent_outs=spent_outs) print("Receiver created payjoin PSBT:\n{}".format( self.wallet_service.human_readable_psbt(r_payjoin_psbt))) signresultandpsbt, err = self.wallet_service.sign_psbt(r_payjoin_psbt.serialize(), with_sign_result=True) assert not err, err signresult, receiver_signed_psbt = signresultandpsbt assert signresult.num_inputs_final == len(receiver_utxos) assert not signresult.is_final print("Receiver signing successful. Payjoin PSBT is now:\n{}".format( self.wallet_service.human_readable_psbt(receiver_signed_psbt))) content = receiver_signed_psbt.to_base64() request.setHeader(b"content-length", ("%d" % len(content)).encode("ascii")) return content.encode("ascii")
#!/usr/bin/env python3 from jmbase import jmprint from jmclient import wallet_tool_main if __name__ == "__main__": jmprint(wallet_tool_main("wallets"), "success")
def tumbler_taker_finished_update(taker, schedulefile, tumble_log, options, res, fromtx=False, waittime=0.0, txdetails=None): """on_finished_callback processing for tumbler. Note that this is *not* the full callback, but provides common processing across command line and other GUI versions. """ if fromtx == "unconfirmed": #unconfirmed event means transaction has been propagated, #we update state to prevent accidentally re-creating it in #any crash/restart condition unconf_update(taker, schedulefile, tumble_log, True) return if fromtx: if res: #this has no effect except in the rare case that confirmation #is immediate; also it does not repeat the log entry. unconf_update(taker, schedulefile, tumble_log, False) #note that Qt does not yet support 'addrask', so this is only #for command line script TODO if taker.schedule[taker.schedule_index + 1][3] == 'addrask': jm_single().debug_silence[0] = True jmprint('\n'.join(['=' * 60] * 3)) jmprint( 'Tumbler requires more addresses to stop amount correlation' ) jmprint( 'Obtain a new destination address from your bitcoin recipient' ) jmprint( ' for example click the button that gives a new deposit address' ) jmprint('\n'.join(['=' * 60] * 1)) while True: destaddr = input('insert new address: ') addr_valid, errormsg = validate_address(destaddr) if addr_valid: break jmprint( 'Address ' + destaddr + ' invalid. ' + errormsg + ' try again', "warning") jm_single().debug_silence[0] = False taker.schedule[taker.schedule_index + 1][3] = destaddr taker.tdestaddrs.append(destaddr) waiting_message = "Waiting for: " + str(waittime) + " minutes." tumble_log.info(waiting_message) log.info(waiting_message) else: # a transaction failed, either because insufficient makers # (acording to minimum_makers) responded in Phase 1, or not all # makers responded in Phase 2, or the tx was a mempool conflict. # If the tx was a mempool conflict, we should restart with random # maker choice as usual. If someone didn't respond, we'll try to # repeat without the troublemakers. log.info("Schedule entry: " + str( taker.schedule[taker.schedule_index]) + \ " failed after timeout, trying again") taker.add_ignored_makers(taker.nonrespondants) #Is the failure in Phase 2? if not taker.latest_tx is None: if len(taker.nonrespondants) == 0: # transaction was created validly but conflicted in the # mempool; just try again without honest settings; # i.e. fallback to same as Phase 1 failure. log.info("Invalid transaction; possible mempool conflict.") else: #Now we have to set the specific group we want to use, and hopefully #they will respond again as they showed honesty last time. #Note that we must wipe the list first; other honest makers needn't #have the right settings (e.g. max cjamount), so can't be carried #over from earlier transactions. taker.honest_makers = [] taker.add_honest_makers( list( set(taker.maker_utxo_data.keys()). symmetric_difference(set(taker.nonrespondants)))) #If insufficient makers were honest, we can only tweak the schedule. #If enough were, we prefer to restart with them only: log.info("Inside a Phase 2 failure; number of honest " "respondants was: " + str(len(taker.honest_makers))) log.info("They were: " + str(taker.honest_makers)) if len(taker.honest_makers) >= jm_single().config.getint( "POLICY", "minimum_makers"): tumble_log.info( "Transaction attempt failed, attempting to " "restart with subset.") tumble_log.info( "The paramaters of the failed attempt: ") tumble_log.info( str(taker.schedule[taker.schedule_index])) #we must reset the number of counterparties, as well as fix who they #are; this is because the number is used to e.g. calculate fees. #cleanest way is to reset the number in the schedule before restart. taker.schedule[taker.schedule_index][2] = len( taker.honest_makers) retry_str = "Retrying with: " + str(taker.schedule[ taker.schedule_index][2]) + " counterparties." tumble_log.info(retry_str) log.info(retry_str) taker.set_honest_only(True) taker.schedule_index -= 1 return #There were not enough honest counterparties. #Tumbler is aggressive in trying to complete; we tweak the schedule #from this point in the mixdepth, then try again. tumble_log.info("Transaction attempt failed, tweaking schedule" " and trying again.") tumble_log.info("The paramaters of the failed attempt: ") tumble_log.info(str(taker.schedule[taker.schedule_index])) taker.schedule_index -= 1 taker.schedule = tweak_tumble_schedule(options, taker.schedule, taker.schedule_index, taker.tdestaddrs) tumble_log.info("We tweaked the schedule, the new schedule is:") tumble_log.info(pprint.pformat(taker.schedule)) else: if not res: failure_msg = "Did not complete successfully, shutting down" tumble_log.info(failure_msg) log.info(failure_msg) else: log.info("All transactions completed correctly") tumble_log.info("Completed successfully the last entry:") #Whether sweep or not, the amt is not in satoshis; use taker data hramt = taker.cjamount tumble_log.info( human_readable_schedule_entry( taker.schedule[taker.schedule_index], hramt)) #copy of above, TODO refactor out taker.schedule[taker.schedule_index][5] = 1 with open(schedulefile, "wb") as f: f.write(schedule_to_text(taker.schedule))
def on_tx_received(self, nick, txhex, offerinfo): if self.txmal: jmprint("Counterparty tx rejected maliciously", "debug") return (False, "malicious tx rejection") return super(DeterministicMaliciousYieldGenerator, self).on_tx_received(nick, txhex, offerinfo)
if not success: jmprint( "Failed to import SNICKER key: {}". format(msg), "error") return False else: jmprint("... success.") # we want the blockheight to track where the next-round rescan # must start from current_block_heights.add( wallet_service. get_transaction_block_height(tx)) # add this transaction to the next round. new_txs.append(tx) if len(new_txs) == 0: return True seed_transactions.extend(new_txs) earliest_new_blockheight = min(current_block_heights) jmprint("New SNICKER addresses were imported to the Core wallet; " "do rescanblockchain again, starting from block {}, before " "restarting this script.".format(earliest_new_blockheight)) return False if __name__ == "__main__": res = main() if not res: jmprint("Script finished, recovery is NOT complete.", level="warning") else: jmprint("Script finished, recovery is complete.")
def main(): parser = OptionParser(usage='usage: %prog [options] walletname', description=description) parser.add_option('-m', '--mixdepth', action='store', type='int', dest='mixdepth', default=0, help="mixdepth to source coins from") parser.add_option('-a', '--amtmixdepths', action='store', type='int', dest='amtmixdepths', help='number of mixdepths in wallet, default 5', default=5) parser.add_option('-g', '--gap-limit', type="int", action='store', dest='gaplimit', help='gap limit for wallet, default=6', default=6) add_base_options(parser) (options, args) = parser.parse_args() load_program_config(config_path=options.datadir) check_regtest() if len(args) != 1: log.error("Invalid arguments, see --help") sys.exit(EXIT_ARGERROR) wallet_name = args[0] wallet_path = get_wallet_path(wallet_name, None) max_mix_depth = max([options.mixdepth, options.amtmixdepths - 1]) wallet = open_test_wallet_maybe( wallet_path, wallet_name, max_mix_depth, wallet_password_stdin=options.wallet_password_stdin, gap_limit=options.gaplimit) wallet_service = WalletService(wallet) # step 1: do a full recovery style sync. this will pick up # all addresses that we expect to match transactions against, # from a blank slate Core wallet that originally had no imports. if not options.recoversync: jmprint("Recovery sync was not set, but using it anyway.") while not wallet_service.synced: wallet_service.sync_wallet(fast=False) # Note that the user may be interrupted above by the rescan # request; this is as for normal scripts; after the rescan is done # (usually, only once, but, this *IS* needed here, unlike a normal # wallet generation event), we just try again. # Now all address from HD are imported, we need to grab # all the transactions for those addresses; this includes txs # that *spend* as well as receive our coins, so will include # "first-out" SNICKER txs as well as ordinary spends and JM coinjoins. seed_transactions = wallet_service.get_all_transactions() # Search for SNICKER txs and add them if they match. # We proceed recursively; we find all one-out matches, then # all 2-out matches, until we find no new ones and stop. if len(seed_transactions) == 0: jmprint("No transactions were found for this wallet. Did you rescan?") return False new_txs = [] current_block_heights = set() for tx in seed_transactions: if btc.is_snicker_tx(tx): jmprint("Found a snicker tx: {}".format( bintohex(tx.GetTxid()[::-1]))) equal_outs = btc.get_equal_outs(tx) if not equal_outs: continue if all([ wallet_service.is_known_script(x.scriptPubKey) == False for x in [a[1] for a in equal_outs] ]): # it is now *very* likely that one of the two equal # outputs is our SNICKER custom output # script; notice that in this case, the transaction *must* # have spent our inputs, since it didn't recognize ownership # of either coinjoin output (and if it did recognize the change, # it would have recognized the cj output also). # We try to regenerate one of the outputs, but warn if # we can't. my_indices = get_pubs_and_indices_of_inputs(tx, wallet_service, ours=True) for mypub, mi in my_indices: for eo in equal_outs: for (other_pub, i) in get_pubs_and_indices_of_inputs( tx, wallet_service, ours=False): for (our_pub, j) in get_pubs_and_indices_of_ancestor_inputs( tx.vin[mi], wallet_service, ours=True): our_spk = wallet_service.pubkey_to_script( our_pub) our_priv = wallet_service.get_key_from_addr( wallet_service.script_to_addr(our_spk)) tweak_bytes = btc.ecdh(our_priv[:-1], other_pub) tweaked_pub = btc.snicker_pubkey_tweak( our_pub, tweak_bytes) tweaked_spk = wallet_service.pubkey_to_script( tweaked_pub) if tweaked_spk == eo[1].scriptPubKey: # TODO wallet.script_to_addr has a dubious assertion, that's why # we use btc method directly: address_found = str( btc.CCoinAddress.from_scriptPubKey( btc.CScript(tweaked_spk))) #address_found = wallet_service.script_to_addr(tweaked_spk) jmprint( "Found a new SNICKER output belonging to us." ) jmprint( "Output address {} in the following transaction:" .format(address_found)) jmprint(btc.human_readable_transaction(tx)) jmprint( "Importing the address into the joinmarket wallet..." ) # NB for a recovery we accept putting any imported keys all into # the same mixdepth (0); TODO investigate correcting this, it will # be a little complicated. success, msg = wallet_service.check_tweak_matches_and_import( wallet_service.script_to_addr(our_spk), tweak_bytes, tweaked_pub, wallet_service.mixdepth) if not success: jmprint( "Failed to import SNICKER key: {}". format(msg), "error") return False else: jmprint("... success.") # we want the blockheight to track where the next-round rescan # must start from current_block_heights.add( wallet_service. get_transaction_block_height(tx)) # add this transaction to the next round. new_txs.append(tx) if len(new_txs) == 0: return True seed_transactions.extend(new_txs) earliest_new_blockheight = min(current_block_heights) jmprint("New SNICKER addresses were imported to the Core wallet; " "do rescanblockchain again, starting from block {}, before " "restarting this script.".format(earliest_new_blockheight)) return False
def outputs_watcher(self, wallet_name, notifyaddr, tx_output_set, unconfirmfun, confirmfun, timeoutfun): """Given a key for the watcher loop (notifyaddr), a wallet name (account), a set of outputs, and unconfirm, confirm and timeout callbacks, check to see if a transaction matching that output set has appeared in the wallet. Call the callbacks and update the watcher loop state. End the loop when the confirmation has been seen (no spent monitoring here). """ wl = self.tx_watcher_loops[notifyaddr] jmprint('txoutset=' + pprint.pformat(tx_output_set), "debug") unconftx = self.get_from_electrum('blockchain.address.get_mempool', notifyaddr, blocking=True).get('result') unconftxs = set([str(t['tx_hash']) for t in unconftx]) if len(unconftxs): txdatas = [] for txid in unconftxs: txdatas.append({ 'id': txid, 'hex': str( self.get_from_electrum('blockchain.transaction.get', txid, blocking=True).get('result')) }) unconfirmed_txid = None for txdata in txdatas: txhex = txdata['hex'] outs = set([(sv['script'], sv['value']) for sv in btc.deserialize(txhex)['outs']]) jmprint('unconfirm query outs = ' + str(outs), "debug") if outs == tx_output_set: unconfirmed_txid = txdata['id'] unconfirmed_txhex = txhex break #call unconf callback if it was found in the mempool if unconfirmed_txid and not wl[1]: jmprint("Tx: " + str(unconfirmed_txid) + " seen on network.", "info") unconfirmfun(btc.deserialize(unconfirmed_txhex), unconfirmed_txid) wl[1] = True return conftx = self.get_from_electrum('blockchain.address.listunspent', notifyaddr, blocking=True).get('result') conftxs = set([str(t['tx_hash']) for t in conftx]) if len(conftxs): txdatas = [] for txid in conftxs: txdata = str( self.get_from_electrum('blockchain.transaction.get', txid, blocking=True).get('result')) txdatas.append({'hex': txdata, 'id': txid}) confirmed_txid = None for txdata in txdatas: txhex = txdata['hex'] outs = set([(sv['script'], sv['value']) for sv in btc.deserialize(txhex)['outs']]) jmprint('confirm query outs = ' + str(outs), "info") if outs == tx_output_set: confirmed_txid = txdata['id'] confirmed_txhex = txhex break if confirmed_txid and not wl[2]: confirmfun(btc.deserialize(confirmed_txhex), confirmed_txid, 1) wl[2] = True wl[0].stop() return
def clientConnectionFailed(self, connector, reason): jmprint('connection failed', "warning") self.bci.start_electrum_proto(None)